manic — the manual
manic is a declarative language for visual explanation. You write a readable text file; Manic turns it into a smooth, deterministic animation. No timeline scrubbing and no hand-managed keyframes — you describe the objects, the relationships, and the story beats while the engine handles motion, continuity, layout, rendering, and repeatable output.
Manic powers visual explanations, animated lessons, simulations, technical diagrams, and creator-ready videos from one readable language. Use it for mathematics, olympiad geometry, calculus, linear algebra, statistics, probability, algorithms and data structures; physics and optics; machine learning and transformer internals; systems architecture and flowcharts; fractals, tilings, fields, and particle processes; SVG artwork and bundled assets; narrated lessons; or polished Reels and Shorts. The same language moves naturally between responsive 2D, textbook-style spatial diagrams, and genuine 3D scenes.
The vocabulary is broad, but the mental model stays deliberately small enough for a non-programmer to read: name what exists, state what it depends on, and describe what should happen. Creators can begin with a circle and a caption, then keep the same model as they progress to live proofs, simulations, neural networks, or cinematic 3D stories.
Manic was created by Anish Nath. Follow its development, examples, and new visual-language experiments on X: @anish2good.
Looking for the website, browser workspace, API, extension, or source code? They are collected on the Manic links page.
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 builds outward from it—first ordinary shapes and motion, then reactive relationships, creator production, specialist kits, systems, simulations, and 3D. The complete generated vocabulary is available in the API reference.
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 full semantic colour on an exact black canvas; compare every option in Templates →.
Manic links
Everything public about Manic, collected in one place.
| Destination | Link | Use it for |
|---|---|---|
| Manic home | maniclang.com | Product overview, downloads, and plans. |
| Manic Create | app.maniclang.com/create | Write, check, preview, and render a Manic project in the browser. |
| Examples gallery | app.maniclang.com/gallery | Browse runnable stories and open one in Create. |
| Manic account | app.maniclang.com/account | Sign in, manage API keys, credits, and billing. |
| Documentation | docs.maniclang.com | Learn the language, CLI, kits, assets, and production workflow. |
| Interactive API reference | docs.maniclang.com/api/ | Explore and try the hosted Platform REST API. |
| Platform API base URL | api.maniclang.com | Base URL for API clients: https://api.maniclang.com/v1/.... |
| Chrome extension | Manic Animate on Chrome Web Store | Turn selected web-page text, equations, and diagrams into an editable Manic project. |
| Source and releases | github.com/maniclang-x/manic | Installation scripts, release archives, engine source, and issue tracking. |
| Updates | @anish2good on X · r/maniclang | Follow releases, examples, and discussion. |
For browser-based creation, start with Manic Create. For automated workflows, create a scoped key in Manic Account, then use the interactive API reference.
Install Manic
Manic is a compiled desktop command-line application. The installer includes
the engine and its reviewed runtime assets; video export also needs ffmpeg.
macOS — Homebrew
This is the simplest macOS installation:
brew install --cask maniclang-x/tap/manic
manic version
Homebrew installs ffmpeg as a dependency and makes manic available on your
PATH.
macOS or Linux — installer
The installer detects the operating system and CPU, downloads the current
release, verifies its SHA-256 checksum, and installs under ~/.local:
curl -fsSL https://raw.githubusercontent.com/maniclang-x/manic/main/install.sh | sh
If needed, add Manic to your shell path:
export PATH="$HOME/.local/bin:$PATH"
Put that line in ~/.zshrc or ~/.bashrc to keep it after opening a new
terminal.
Windows — PowerShell
irm https://raw.githubusercontent.com/maniclang-x/manic/main/install.ps1 | iex
Open a new terminal after installation. Install ffmpeg separately and make
sure ffmpeg.exe is on PATH before exporting video or GIF files.
Linux server or container
Live preview needs a display. The Linux archive includes a helper for Ubuntu and EC2 that installs Xvfb, software OpenGL, ffmpeg, and headless audio support:
sudo bash /usr/local/share/manic/setup/ubuntu-headless.sh
manic-render story.manic --record out
If the archive was installed under ~/.local, use the corresponding helper
under ~/.local/share/manic/setup/.
Confirm the installation
manic version
manic status
manic check story.manic
check parses and validates without opening a window or consuming a preview.
Connect a Manic account
You can try Manic before signing in. Connect the installation when you want it to use your account’s preview and export balance:
manic login
manic status
Verify that the confirmation code in the browser matches the terminal. Manic source files are rendered locally; ordinary licence accounting does not upload the story source.
Upgrade
Homebrew:
brew update
brew upgrade --cask manic
Installer users can rerun the installation command. A normal reinstall should
keep ~/.manic, which holds this device’s installation identity and offline
accounting state.
For platform-specific prerequisites, manual archives, and security warnings, see the public installation reference and troubleshooting guide.
Next: use the Manic command line →
Command-line guide
The Manic CLI follows one short loop: check → preview → export.
manic check story.manic
manic story.manic
manic story.manic --record out
The last command writes out/out.mp4 and timing markers under out/.
Everyday commands
| Command | Purpose |
|---|---|
manic check story.manic | Parse and validate without opening a window |
manic fix story.manic | Apply safe mechanical source fixes in place |
manic story.manic | Open an interactive preview from t=0 |
manic story.manic --record out | Export a final MP4 |
manic story.manic --still 2.5 | Export the frame at 2.5 seconds |
manic story.manic --gif --record out | Export an animated GIF |
manic stages story.manic | List named stages and their durations |
manic voice story.manic | Report narration/TTS usage without synthesis |
manic svgcheck assets/ | Screen SVG files for importer compatibility |
manic ui | Open the Workbench — a local browser studio for your project |
manic version | Show the installed version |
manic --help | Show every command and option |
The Workbench UI
manic ui opens the Manic Workbench — a local, browser-based studio for your
.manic projects. It’s a friendlier front-end to the same engine you drive from the
command line:
- Files — browse, create, rename, duplicate, and delete stories, edited in a Monaco editor with syntax help, live diagnostics, and autosave.
- Preview & render — one-click preview, and MP4 / animated GIF / PNG-frame exports
with live progress and a render history under the project’s
.manic-outputfolder. - AI drafting (optional) — describe a story and get a draft, with diff-before-apply
and a built-in
manic check. Nothing leaves your machine unless you enable a provider and add your own key.
manic ui # open the current folder
manic ui path/to/project # open a specific project folder
It runs entirely on your machine, on a loopback-only address with a session token, and
points the Workbench at this manic binary and its bundled assets automatically — no
flags needed. Use Open folder… in the UI to switch projects without restarting.
Requirements. The Workbench is fetched and run with npx, so it needs Node.js 22
or newer (npm and npx come with it); FFmpeg on your PATH is needed for MP4/GIF export.
Verify with:
node -v # v22.x or newer
npm -v
npx -v
If Node is missing or too old, manic ui prints a short install guide for your operating
system instead of launching — install Node.js 22+, reopen your terminal, and run it again.
Render one part of a story
Named stages make long explainers fast to review:
manic stages lesson.manic
manic lesson.manic --stage proof
manic lesson.manic --from-stage setup --to-stage result --record review
You can also select a time range:
manic lesson.manic --from 4.0 --to 9.5 --record excerpt
Canvas and quality
manic story.manic --canvas portrait
manic story.manic --record out --fps 30
manic story.manic --record out --scale 1.25
The default studio preset records at 60 FPS. Useful presets are:
studio— full-quality branded MP4, the default;reel— full-quality social/vertical output;test— fast 30 FPS verification;farm— 30 FPS headless/software-rendering output.
Choose one explicitly:
manic story.manic --record out --preset reel
xvfb-run -a manic story.manic --record out --preset farm
xvfb-run provides a virtual display; it does not change Manic’s FPS.
Account and offline commands
manic login
manic status
manic sync
manic logout
statusshows the authoritative online balance when connected and reachable.syncrefreshes grants and reconciles completed offline work.logoutuses browser confirmation before disconnecting the installation.
Preview runs and successful exports are separate allowances. manic check,
validation failures, and failed display setup consume neither.
Deterministic variations
Generated scenes use deterministic seeds by default. Reproduce or vary them:
manic story.manic --seed 42
manic story.manic --seed random
Where files and assets live
The standard runtime asset catalogue ships beside the executable. Ordinary
installs do not need MANIC_ASSETS_DIR. Set it only when intentionally
replacing the entire bundled asset root:
export MANIC_ASSETS_DIR=/absolute/path/to/custom/assets
Now create the first scene: Getting started →
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. Then use Reactive stories to keep equations, plots, diagrams, and captions synchronized across each explanation beat. Want spoken narration? See Voice — optional, one
voice(...)plusspeak("…")lines.
// 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 full-colour
black template automatically. See Templates when you want
mono, paper, blueprint, terminal, or a tinted 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 →
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 — filled + outlined by default |
| rect | rect(box, (cx, cy), 200, 120); | a rectangle — filled + outlined by default |
| line | line(edge, (100, 100), (400, 300)); | a line from point to point (stroke only) |
| arrow | arrow(v, (100, 400), (400, 400)); | a line with an arrowhead at the end (stroke only) |
| dot | dot(p, (cx, cy), 8); | a small filled disc (no outline) — hides crossings |
| text | text(cap, (cx, 640), "hello"); | a text label anchored at a point |
Plus composites: polygon / sector / annulus / pie (filled by default),
arc / brace / ellipse (stroke only), and boolean regions
(union / intersect / …).
▶ See it play: SHAPECRAFT — fill vs
outlined, the cast, palette → become, spin-wound morph, a disc that
splits into four colours → reunites → moves, and two-colour booleans.
Ordinary text is portable and shaped automatically
text, say, captions, labels, and kit-generated words use Manic’s embedded
font engine. No machine font is consulted, and there is no font-selection DSL:
choose only the semantic mono, bold, or display look. Manic handles
fallback, ligatures, combining marks, bidirectional order, wrapping, reveal,
rotation, glow, camera zoom, and recording from one shaped layout.
Arabic and Devanagari are bundled script examples, so these work unchanged in preview, backend recording, and the browser build:
text(arabic, (cx, 300), "التعلّم يجعل الأفكار واضحة");
text(hindi, (cx, 420), "ज्ञान से प्रकाश मिलता है");
Use equation or inline $…$ for mathematical typesetting. Run manic check
before rendering; an unsupported Unicode cluster names the entity and code
point instead of silently drawing a replacement box. Colour emoji remains
intentionally unsupported until colour-font rendering is portable.
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); // default random layout
rect(tank, (750,300), 180, 160);
particles(samples, tank, 24, 4, 11, "grid");
link(pipe, glass, tank, 35); // 35 px of curve; 0 is straight
circle(orbit, (980,300), 90);
particles(ticks, orbit, 24, 4, 13, "ring");
particles works inside circles and rectangles. Its bare id addresses every
child dot, so hidden(bubbles) and recolor(bubbles, fg) affect the whole
group. Use the optional "grid" layout for an ordered rectangular state, then
arrange(samples, tank, "random", 2, smooth) to move the same dots into a
typical random state. arrange may target a larger container for expansion and
can return to "grid" exactly. A circle plus "ring" gives the particles an
ordered radial endpoint. link meets 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.
For a derivation, keep that same equation id and animate only the change:
rewrite(step, `\mathrm{slope}=\frac{\mathrm{rise}}{\mathrm{run}}`, 0.9, smooth);
rewrite(step, `\mathrm{slope}=\frac{x}{1}`, 0.9, smooth);
Manic matches the rendered LaTeX parts; it does not perform algebra. This makes the verb generic across algebra, calculus, physics, sums, fractions, radicals, and creator scenes while keeping each settled formula exact.
// 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); |
dashed(id, [dash], [gap]) | dashed path or outlined circle (defaults 16/10 px) | dashed(mark, 4, 3); |
gradient(id, c1, c2, ..., [mode]) | multi-stop gradient: along a path stroke, a linear (angle°) / radial fill, or a computed "speed"/"curvature" quantity on strokes | gradient(wave, blue, cyan, gold, 270); |
size(id, n) | text size | size(cap, 30); |
glow(id, n) | neon halo strength | glow(sun, 8); |
opacity(id, 0..1, [layer]) | transparency; add fill or stroke to set just that layer, independent of the other | opacity(box, 0.4, fill); |
filled(id) / outlined(id) | solid (fill on, rim off) / hollow (rim on, fill off) | outlined(box); |
outline(id, c) | set the rim colour (and turn the rim on) — does not drop the fill | outline(box, gold); |
hue(id, deg) | colour by an angle (0–360) — per-entity colour in 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); |
Textbook diagrams: prefer
outlined(or a thinarc) for angle marks and “regions of interest.” Afilleddisc or sector sitting on top of axes and construction lines hides what’s underneath — even at low opacity. Reserve solid fills for backdrop wells behind the diagram, not overlays.
circledefaults to filled + outlined, with a soft slate rim (#9AA6C9) that reads cleanly over axes. For math constructions always pairoutlined(id)—outline(id, color)alone only recolors the rim and leaves the fill on. Contact points that land on crossings should be open / dashed rings (outlined+dashed), notdot— a solid marker paints over the intersection you are trying to show.
A shape owns its fill and its rim separately.
color(id, c)sets the fill,outline(id, c)sets the rim, andopacitycan target one layer:opacity(id, 0.4, fill)gives a translucent interior under a crisp rim — a filled shape you can still see the construction through — whileopacity(id, v, stroke)fades just the rim. On a hollow shape (outlined) the fill is off, so colour the visible ring withoutline, notcolor. (Paths —line,arrow,ellipse— are stroke-only:colorsets their stroke and there is no fill layer.)
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,gold,red,orange,blue,teal,violet,coral,indigo,mint,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.
Relationship motion
Three verbs cover motion that would otherwise require many coordinated keyframes:
attach(label, marker, (0,-36)); // label follows marker
travel(marker, route, 2, smooth);
become(marker, nodeBlueprint, 0.8); // same id, new visual state
attach(label, none); // release at the settled position
turn(nodeGroup, marker, 20, 0.6, out);// shared group pivot
attach is persistent until released. become interpolates compatible shapes
and safely crossfades other pairs, always settling on the exact declared
blueprint. turn accepts one entity or a tag and rotates its members around a
point or entity pivot. See Motion graphics for the full
design rules and a copyable V2 Reel.
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.
travel(marker, path, dur, smooth) moves the actual marker once along that
same kind of path and leaves it at the endpoint. Use flow for temporary
emphasis and travel when object identity and the stopping point matter:
plot(curve, (180,620), 90, 140, "1-exp(-x)", (0,4));
dot(marker, (180,620), 7);
par { draw(curve, 2, out); travel(marker, curve, 2, out); }
For a persistent layout change, keep the same particle ids:
rect(chamber, (540,500), 600, 260);
particles(gas, chamber, 48, 5, 17, "grid");
arrange(gas, chamber, "random", 2.5, smooth);
arrange(gas, chamber, "grid", 2.5, smooth); // exact ordered return
circle(orbit, (900,500), 150);
arrange(gas, orbit, "ring", 1.2, smooth); // move and stop radially
arrange also accepts a different circle/rectangle container, which makes a
free expansion readable without moving every dot by hand. A random layout
uses independent seeded curved routes: the result is reproducible and
scrubbable, but does not look like every particle followed the same ruler.
For a complete creator-oriented treatment of persistent motion, arrival,
settling, and the difference between travel and flow, see
Motion graphics — move ideas, not layers.
For shape continuity, morph(a,b) preserves topology: graph/line paths remain
open, while circles and other closed outlines remain closed. This prevents an
unwanted diagonal chord when a plot becomes a connector or guide line.
For a persistent comparison style, use dashed(id, [dash], [gap]) before the
timeline. It is a base Manic modifier—not a calculus feature—so the same
16/10-pixel default pattern works on a plot, guide line, link, arrow, curve,
spline, coil, or plain arc. Increase both values for a calmer large-format dash;
keep the gap smaller than the dash when the curve must remain easy to follow.
Content & colour
say(cap, "next step"); // -> crossfade a text entity to new words
recolor(sun, lime, 0.5); // -> permanently animate the colour
Rewrite an equation without rebuilding it
Declare one real LaTeX equation, then supply each mathematically correct state:
equation(work, (cx, 300), `x^2+2x=3`, 54);
rewrite(work, `x^2+2x+1=4`, 0.9, smooth);
rewrite(work, `(x+1)^2=4`, 0.9, smooth);
rewrite(work, `x=-1\pm2`, 0.9, smooth);
rewrite is visual, not a computer-algebra system: it never invents or verifies
a step. Equal RaTeX parts retain identity and travel smoothly. Matching respects
reading order, coarse layout roles—main line, exponent, numerator, denominator,
fraction rule, and other structure—and RaTeX math-style depth. An identical
character therefore cannot change mathematical jobs or jump between levels of
a nested exponent. This also protects logarithm bases, operator limits, and
derivative orders. If one side gains or loses fraction, radical, or
grouping structure, that side leaves before the replacement enters while the
compatible side and equality remain continuous. Globally incompatible states,
such as a matrix becoming a compact formula, use a staged whole-equation
dissolve with only a short, dim overlap. When unmatched source and target glyphs
are a replacement, the old glyph leaves before the new one becomes readable;
pure additions still enter immediately.
Semantic \textcolor roles follow the authored formula, and the final frame is
always the exact target LaTeX. No flag or alternate verb is required. Keep one
equation id for the whole derivation, write readable steps, and use wait
between them when the viewer needs time to absorb a result.
For a plot or diagram that changes with the same step, place rewrite and the
related motion in par. Existing equations are unaffected unless this verb is
used.
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 |
step("name") { … } | all at once, with a named exported start | change one reactive world across representations |
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.
Named reactive steps
Use step when a beat represents the world’s next conceptual state, not
just anonymous timing. Its children start together like par; its duration is
the longest child; anything not mentioned remains exactly as it was. The unique
name is written to markers.json and becomes a first-class editing boundary:
manic stages FILE.manic lists durations, --stage NAME previews or records
one stage, and --from-stage / --to-stage export an inclusive arc.
step("explain") {
rewrite(work, `f'(x)=2x`, 0.9, smooth);
to(tangent, x, 2.5, 2.0, smooth);
to(slopeValue, x, 2.5, 2.0, smooth);
say(caption, "Every representation changes together.");
}
Steps are top-level and names must be non-empty and unique. Put seq { … }
inside a step when a small part needs ordered choreography.
An authored wait after a step remains part of that stage until the next step
begins. This lets a stage-only export keep the reading hold after its motion.
See Story stages for the live navigator and publishing
workflow.
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");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
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.
Colour & style
The palette
manic uses a small set of semantic colour names. The default black template
shows their full hues on an exact black canvas. Choose mono when the scene
should communicate through black-and-white luminance instead. The semantic
roles are:
| name | is | name | is |
|---|---|---|---|
cyan | electric blue | gold | warm amber |
magenta | hot pink | red | warm red |
lime | acid green | orange | orange |
blue | true blue (≠ cyan) | teal | cyan–green |
violet | true purple | coral | soft warm red-orange |
indigo | deep blue–violet | mint | soft cool green |
dim | muted grey-violet | fg | near-white (default text) |
void | the background | panel | dark fill |
Aliases: pink/accent → magenta · green → lime · yellow/amber → gold ·
purple → violet · turquoise → teal · salmon → coral · seafoam → mint ·
azure → blue · crimson → red · gray/grey → dim · white → fg · bg → void.
color(sun, cyan);
recolor(sun, magenta, 0.5); // animate to a new palette colour
Any colour, by hue
For a computed colour — 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.
Gradient paint — computed, not painted
For a colour that reads a quantity, use gradient. One word covers multi-stop
ramps on fills and strokes; the mode (optional) picks the truth:
gradient(wave, blue, cyan, gold, 270); // height of a plot
gradient(path, magenta, cyan); // arc length along a stroke
gradient(well, panel, void, radial); // centre → edge on a fill
gradient(p.path, blue, cyan, gold, "speed"); // true local speed (physics traj.)
gradient(swoop, dim, magenta, "curvature"); // how hard a path bends
Stops are palette names (≥2, evenly spaced) and stay template-aware.
"speed" only works on pre-simulated physics trajectories; "curvature" works
on any path. See the Modifiers table in Shapes, and the demos
gradient,
gradient-fastest-descent, and the
shorts gradient-fastest-descent-shorts /
gradient-pendulum-shorts.
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: 4:5, square, portrait/9:16, 4:3, 1080p, 4k)
canvas(1280, 720); // explicit
portrait / 9:16 is 1080×1920 — pair it with the reel render preset for
vertical / social clips.
Use --canvas portrait|4:5|square|16:9|WIDTHxHEIGHT to reframe one responsive
source without changing its canvas(...) line. The override is applied before
w, h, cx, cy, and build-time layout branches are evaluated.
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 |
|---|---|
black | default — full semantic colour on exact black, no chrome |
mono (aliases monochrome, bw) | black-and-white editorial, near-black page, subtle glow |
plain (aliases blank, clean) | original neon palette on an indigo-black surface, 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("black").
Use mono deliberately for grayscale and plain for the original tinted neon
surface.
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.
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("black");
black is the default. If a DSL file does not call template(...), it renders
exactly as if template("black") 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 |
|---|---|---|
black | creator stories, vivid explainers and general authoring | exact #000000 canvas, full semantic colour, no chrome |
mono | professional explainers, proofs and restrained Reels | black-and-white editorial surface, clear luminance hierarchy, subtle glow |
plain (alias blank) | dark indigo neon scenes | original neon semantic colours on a subtly tinted near-black surface |
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; blank and clean for plain; light and print for paper; and
blue for blueprint.
black and plain are intentionally different. black is a truly black,
chrome-free creator canvas; plain retains Manic’s original indigo-black neon
surface.
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, blue, teal, violet, coral,
indigo, and mint 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 black when colour is part of the explanation. It gives Creator stories a clean, cinema-dark canvas while preserving semantic accents:
canvas("9:16");
// template("black") is optional because black is the default.
creator(me, "@anish2good yt=zarigatongy x=@anish2good web=maniclang.com footer=social");
quiz(q, "Which statement is true?");
Choose mono for an intentionally black-and-white identity, shorts for a
tinted studio surface, or 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.
- On black, inspect dim lines and labels as well as bright accents.
- 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 you specifically want the original indigo-neon surface. - Use
paperorblueprintto test whether a construction remains legible on a very different surface.
Next: Colour, glow and semantic roles →
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);
Curve L-systems — one rule, one path
For deterministic textbook curves, use lsystem instead of manually recursing
into thousands of separate lines:
lsystem(carpet, (cx,cy), 620,
"F+F+F+F",
"F=FF+F+F+F+FF",
"angle=90 heading=0 iterations=4");
gradient(carpet, cyan, magenta, gold);
stroke(carpet, 2);
untraced(carpet);
draw(carpet, 3, smooth);
The six arguments are:
- id, centre, and fitted square size;
- the starting string (axiom);
- semicolon-separated rewrite rules such as
"X=...;Y=..."; - one short options string.
In the generated program, + turns left by angle, - turns right, |
turns around, and every symbol listed by draw=F moves forward while drawing.
heading sets the first direction, iterations controls detail, and padding
keeps the result away from its fitted edge.
Use closed=true fill=true when the grammar describes a closed boundary; Manic
triangulates it into one concave region. Keep fill=false for line curves. The
constructor deliberately does not interpret branch brackets: use a recursive
def or tree3 for branching plants. This separation keeps curve stories
simple and makes invalid input easy to repair.
Practical tips
- Increase
iterationsone step at a time: rewrite systems grow exponentially. - Keep the curve as one entity, then use ordinary
draw,gradient,move,become,step, and camera verbs. - For a lesson, show generations 0 → 1 → 2 before revealing the detailed curve.
- See
lsystem-asymptote-curves.manicfor four canonical grammars andcreator-lsystem-fractal-curve.manicfor a complete creator Short.
Repeated motifs — one design, many placements
Use repeat when the visual idea is “this motif forms a field,” not “write a
loop that manually copies every part.”
polygon(tile, (cx-12,cy+10), (cx+12,cy+10), (cx,cy-14));
repeat(field, tile, "layout=hex rings=4 spacing=42 rotate=30 scale=0.8");
untraced(field);
draw(field, 2, smooth);
The motif can be one entity or a tag containing several entities. Choose:
layout=hex rings=N spacing=S;layout=grid rows=R cols=C spacing=S(or separategapx/gapy);layout=radial count=N radius=R face=same|out.
rotate turns the arrangement and scale sizes each instance. Every output is
tagged with the destination id; stable instances are {id}.i0, {id}.i1, …
The output of one repeat can itself be the motif of another, which is how
nested textbook tilings stay short. Use normal color, gradient, mask,
untraced, draw, and stagger afterward.
See asymptote-tiling-reference.manic for hex/grid/radial/nested coverage and
creator-one-tile-pattern-story.manic for the creator treatment.
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.
Bundled assets — portable files without path guessing
Manic scenes can use small, documented production assets through a stable
asset: URI. The URI works from the desktop CLI, a different working
directory, the Docker image, or the production backend. No render flag is
needed.
image(mark, (cx, 260), "asset:manic-logo.png", 180, 180);
model3(beacon, "asset:models/manic-pyramid.obj", (0,0,0), 1);
assembly3(console, "asset:models/manic-console.obj", (3,0,0), 1);
svg(logo, (cx, 480), "asset:svg/heart.svg", 320);
The same image URI can be used in a Creator profile:
creator(me, "@anish2good name=Manic logo=asset:manic-logo.png footer=signature");
socials(me);
Available public assets
| Stable URI | Kind | Useful for |
|---|---|---|
asset:manic-logo.png | PNG | image(...), Creator logo=, a Manic-branded example |
asset:models/manic-pyramid.obj | Geometry-only OBJ | model3(...), a beacon, monument, marker, or placeholder model |
asset:models/manic-console.obj | Grouped geometry-only OBJ | assembly3(...), addressable base/screen/key parts, staged technical stories |
asset:svg/lucide/<name>.svg | SVG icons — Lucide (ISC) | svg(...), themeable line icons (recolour to taste) |
asset:svg/heroicons/<name>.svg | SVG icons — Heroicons (MIT) | svg(...), solid glyphs |
asset:svg/flags/<cc>.svg | SVG flags — flag-icons (MIT) | svg(...), country flags in real colours |
asset:svg/emoji/<name-or-codepoint>.svg | SVG emoji — Twemoji (CC-BY 4.0) | svg(...), flat colour emoji; fire.svg/1f525.svg |
asset:svg/heart.svg · pin.svg · robot.svg · map-region.svg | SVG demo art (CC0) | svg(...), the import demos |
asset:svg/physics/effusion-reservoir.svg | SVG scientific apparatus (CC0) | reusable thermal-reservoir/nozzle shell for process stories |
asset:svg/physics/rocket-cutaway.svg | SVG scientific apparatus (CC0) | cutaway launch vehicle for payload, propellant, combustion, nozzle, and thrust stories |
asset:svg/physics/rocket-first-stage.svg | SVG scientific apparatus (CC0) | independently movable launch booster |
asset:svg/physics/rocket-second-stage.svg | SVG scientific apparatus (CC0) | independently movable second stage |
asset:svg/physics/rocket-upper-stage.svg | SVG scientific apparatus (CC0) | payload fairing and upper stage |
The SVG sets are a curated, permissively-licensed starter pack (see
assets/svg/LICENSES.md). List what’s bundled — and what a file imports as — with
manic svgcheck assets/svg.
The catalog stays deliberately small. Do not invent an asset name that is not
listed here. Manic reports a clear error if a bundled URI is missing, and it
rejects .. traversal. OBJ files also retain the normal file-size and geometry
safety limits.
Vector import — SVG as native geometry
svg() brings vector artwork in as native path entities — not a texture. Each
subpath of the SVG becomes a real Polyline/Polygon you can draw, morph, recolour,
and animate exactly like a shape you authored by hand. It’s the 2D twin of model3’s
OBJ import.
svg(logo, (cx, cy), "asset:svg/heart.svg", 320);
Reposition — the second argument is the centre. Move it, or animate it; every piece follows because the whole drawing shares one tag:
svg(pin, (cx, cy), "asset:svg/pin.svg", 200); // centred at (cx, cy)
move(pin, (cx, cy - 200), 1.0); // glide the whole drawing up
Resize — the last argument is the width in pixels; the height follows the artwork’s own aspect ratio:
svg(small, (300, cy), "asset:svg/robot.svg", 120); // 120 px wide
svg(big, (800, cy), "asset:svg/robot.svg", 360); // same file, 3× bigger — still crisp
Recolour — imported fills reproduce faithfully (they are not re-themed by the template, even if a colour happens to match a palette name). Change them whenever you like:
recolor(logo, cyan); // whole drawing → cyan
hue(logo, 200, 0.9, 0.6); // …or by HSL
gradient(logo, gold, magenta); // …or a gradient across it
Animate — one verb drives the whole drawing (it is tagged with the id):
hidden(logo); show(logo, 0.8); // reveal
pulse(logo, 0.9); // emphasise
Address one piece — subpaths are named {id}.p0, {id}.p1, … in document
order, so you can single one out:
recolor(pin.p1, gold); // just the pin's inner hole
Layering — imported art defaults to z = 0. Raise it above another layer
(a grid, a backdrop) with z:
z(logo, 6);
What v1 imports (and what it skips)
Imported: paths plus the basic shapes (rect, circle, ellipse, line,
polygon, polyline) with their transforms resolved, and solid fill/stroke
colours. Curves are flattened to smooth polylines.
Imported gradients: linear and radial fill gradients come in faithfully (mapped
onto Manic’s gradient(), multi-stop ramps resampled) — so gradient icons,
illustrations, and emblems keep their shading.
Skipped for now: SVG <text> and embedded <image>, clipPath/mask, filters and
blurs, pattern paint, and gradient strokes (fills are supported). Keep artwork
reasonably simple — there’s a point-budget guard and the usual file-size limit.
Two things worth knowing:
currentColor— most icon sets (Lucide, Heroicons, …) paint withcurrentColor, a host-themed placeholder. Manic imports those as themeable geometry: they take the entity’s own colour, sorecolor(icon, cyan)just works. Artwork with real colours (flags, illustrations) keeps them faithfully.- Concave fills are triangulated (ear-clipping), so stars, notched glyphs, and blobs fill correctly — not just convex shapes.
Screen a set before you rely on it
Downloaded a folder of SVGs? Vet them against the importer first:
$ manic svgcheck assets/svg/lucide
✓ rocket.svg — 4 paths, 199 pts, themeable (0 with fill/stroke)
✓ map-pin.svg — 2 paths, 147 pts, themeable (0 with fill/stroke)
…
Each file reports its subpath/point count and whether it’s themeable (recolour it)
or coloured (keeps its own colours); anything empty, over-budget, or unparseable is
flagged with ✗. scripts/fetch-svg-assets.sh vendors the bundled sets the same way.
Your own images and models
Ordinary paths still work:
image(photo, (cx,cy), "uploads/my-photo.jpg", 720, 480);
model3(product, "uploads/my-product.obj", (0,0,0), 1);
svg(logo, (cx,cy), "uploads/my-logo.svg", 300);
Bring in any SVG this way — an icon you downloaded, a logo, a map outline. The
caller must make those files available to the renderer. This is the right
choice for uploads and private brand assets; asset: is for the small catalog
that ships with Manic.
Adding a new bundled asset to Manic
- Put it under
assets/in a typed folder such asmodels/, with a lowercase, descriptive filename. - Add its stable URI to this page and
assets/README.md. - Add or update a checked
.manicexample using the URI. - Keep models geometry-only; do not add scripts, arbitrary shaders, or remote dependencies.
- Run the tests and mdBook build.
The release machinery copies the entire directory. Docker installs it at
/usr/local/share/manic/assets; Linux builds produce
dist/manic-assets.tar.gz; the EC2 deploy installs that archive; and the
playground sync mirrors the same catalog. Future entries therefore need no
per-file pipeline edit. A custom deployment may point MANIC_ASSETS_DIR at a
different catalog root.
SVG assets — reference & aliases
Every svg() import takes an asset:svg/<set>/<name>.svg URI (or your own
file path). The bundled catalogue covers 6,404 shapes across four sets,
vendored by scripts/fetch-svg-assets.sh and screened with
manic svgcheck assets/svg. Licences: assets/svg/LICENSES.md.
Reference notation
svg(logo, (cx, cy), "asset:svg/lucide/rocket.svg", 300); // an icon
svg(fin, (cx, cy), "asset:svg/flags/in.svg", 220); // a flag (ISO code)
svg(joy, (cx, cy), "asset:svg/emoji/fire.svg", 140); // an emoji (alias)
svg(joy, (cx, cy), "asset:svg/emoji/1f525.svg", 140); // …same, by codepoint
| Set | Count | Naming | Licence | Imports as |
|---|---|---|---|---|
lucide/ | ~1,756 | icon name (cpu, database, map-pin, …) | ISC | themeable outline (recolour it) |
heroicons/ | ~324 | icon name (bolt, beaker, rocket-launch, …) | MIT | themeable solid glyph |
flags/ | ~271 | ISO 3166-1 alpha-2 (us, in, jp, de, …) | MIT | full colour |
emoji/ | ~4,009 | Unicode codepoint (1f525.svg) or friendly alias | CC-BY 4.0 | full colour |
Themeable vs faithful. Lucide/Heroicons paint with currentColor, so they
take the entity’s colour — recolor(icon, cyan) just works. Flags and emoji keep
their real colours. Concave shapes (stars, emblems) fill correctly, and
stroke-drawn designs (stripes, Nordic crosses) import as filled bands.
Known limitation. A few flags place their stars with SVG
<use>instancing (e.g.cn— China) or<marker>; those instanced glyphs render incorrectly for now (tracked). The flag’s plain fields still show, and all icons/emoji and most flags render cleanly.svgchecka set before you rely on it.
Emoji friendly aliases
Twemoji files are named by Unicode codepoint; these short names resolve to the same file — either works:
| Alias | Codepoint | Alias | Codepoint | Alias | Codepoint | ||
|---|---|---|---|---|---|---|---|
grinning | 1f600 | heart | 2764 | sun | 2600 | ||
smile | 1f604 | fire | 1f525 | moon | 1f319 | ||
joy | 1f602 | rocket | 1f680 | cloud | 2601 | ||
wink | 1f609 | star | 2b50 | zap | 26a1 | ||
thumbsup | 1f44d | trophy | 1f3c6 | rainbow | 1f308 | ||
thumbsdown | 1f44e | party | 1f389 | earth | 1f30d | ||
check | 2705 | brain | 1f9e0 | target | 1f3af | ||
cross | 274c | bulb | 1f4a1 | flag | 1f6a9 | ||
warning | 26a0 | eyes | 1f440 | hourglass | 231b | ||
thinking | 1f914 | clap | 1f44f | chart | 1f4c8 | ||
sob | 1f62d | wave | 1f44b | computer | 1f4bb | ||
muscle | 1f4aa | money | 1f4b0 | phone | 1f4f1 | ||
lock | 1f512 | key | 1f511 | mail | 1f4e7 | ||
bell | 1f514 | door | 1f6aa | car | 1f697 | ||
goat | 1f410 |
Any other emoji works by codepoint — e.g. asset:svg/emoji/1f680.svg (🚀). Add
your own aliases in scripts/fetch-svg-assets.sh.
Discovering the full catalogue
Browse every bundled asset: the SVG asset catalogue lists
all 6,400+ files by set — searchable with ⌘/Ctrl-F, no CLI needed. That page is
generated from the assets themselves by scripts/gen-svg-index.py, so it always
matches what ships.
If you have the CLI, svgcheck also screens a folder — and shows what each file
imports as (paths, points, themeable vs coloured):
$ manic svgcheck assets/svg/lucide
✓ rocket.svg — 4 paths, 199 pts, themeable
✓ map-pin.svg — 2 paths, 147 pts, themeable
…
$ manic svgcheck assets/svg/flags # every country flag
$ manic svgcheck assets/svg/emoji # every Twemoji
scripts/fetch-svg-assets.sh re-vendors or updates every set from its upstream
repo (Lucide · Heroicons · flag-icons · Twemoji). Bring your own:
svg(id, pos, "path/to/any.svg") imports any SVG file — vet it with svgcheck
first. See the import how-to for
positioning, resizing, recolouring, and animating.
SVG asset catalogue
Every one of the 6,429 bundled SVG vectors, by set — so you can find an asset without the CLI. Drop any of them in with:
svg(id, (x, y), "asset:svg/<set>/<name>.svg", size);
Names are searchable — use your browser’s find (⌘/Ctrl-F). Licences are per set; see assets/svg/LICENSES.md for full attribution. For a quick start with plain-English emoji names, see the friendly aliases.
| Set | Count | Licence | Path |
|---|---|---|---|
| Lucide | 1,756 | ISC | asset:svg/lucide/<name>.svg |
| Heroicons | 324 | MIT | asset:svg/heroicons/<name>.svg |
| Flags | 271 | MIT | asset:svg/flags/<name>.svg |
| Emoji | 4,073 | CC-BY 4.0 | asset:svg/emoji/<name>.svg |
| Physics | 5 | CC0 | asset:svg/physics/<name>.svg |
Lucide — clean outline icons · 1,756 · ISC
asset:svg/lucide/<name>.svg
a-arrow-down · a-arrow-up · a-large-small · accessibility · activity · ad · air-vent · airplay · alarm-clock · alarm-clock-check · alarm-clock-minus · alarm-clock-off · alarm-clock-plus · alarm-smoke · album · align-center-horizontal · align-center-vertical · align-end-horizontal · align-end-vertical · align-horizontal-distribute-center · align-horizontal-distribute-end · align-horizontal-distribute-start · align-horizontal-justify-center · align-horizontal-justify-end · align-horizontal-justify-start · align-horizontal-space-around · align-horizontal-space-between · align-start-horizontal · align-start-vertical · align-vertical-distribute-center · align-vertical-distribute-end · align-vertical-distribute-start · align-vertical-justify-center · align-vertical-justify-end · align-vertical-justify-start · align-vertical-space-around · align-vertical-space-between · ambulance · ampersand · ampersands · amphora · anchor · angry · annoyed · antenna · anvil · aperture · app-window · app-window-mac · apple · archive · archive-restore · archive-x · armchair · arrow-big-down · arrow-big-down-dash · arrow-big-left · arrow-big-left-dash · arrow-big-right · arrow-big-right-dash · arrow-big-up · arrow-big-up-dash · arrow-down · arrow-down-0-1 · arrow-down-1-0 · arrow-down-a-z · arrow-down-from-line · arrow-down-left · arrow-down-narrow-wide · arrow-down-right · arrow-down-to-dot · arrow-down-to-line · arrow-down-up · arrow-down-wide-narrow · arrow-down-z-a · arrow-left · arrow-left-from-line · arrow-left-right · arrow-left-to-line · arrow-right · arrow-right-from-line · arrow-right-left · arrow-right-to-line · arrow-up · arrow-up-0-1 · arrow-up-1-0 · arrow-up-a-z · arrow-up-down · arrow-up-from-dot · arrow-up-from-line · arrow-up-left · arrow-up-narrow-wide · arrow-up-right · arrow-up-to-line · arrow-up-wide-narrow · arrow-up-z-a · arrows-up-from-line · asterisk · astroid · at-sign · atom · audio-lines · audio-waveform · award · axe · axis-3d · baby · backpack · badge · badge-alert · badge-cent · badge-check · badge-dollar-sign · badge-euro · badge-indian-rupee · badge-info · badge-japanese-yen · badge-minus · badge-percent · badge-plus · badge-pound-sterling · badge-question-mark · badge-russian-ruble · badge-swiss-franc · badge-turkish-lira · badge-x · baggage-claim · balloon · ban · banana · bandage · banknote · banknote-arrow-down · banknote-arrow-up · banknote-check · banknote-x · barcode · barrel · baseline · bath · battery · battery-charging · battery-full · battery-low · battery-medium · battery-plus · battery-warning · beaker · bean · bean-off · bed · bed-double · bed-single · beef · beef-off · beer · beer-off · bell · bell-check · bell-dot · bell-electric · bell-minus · bell-off · bell-plus · bell-ring · between-horizontal-end · between-horizontal-start · between-vertical-end · between-vertical-start · biceps-flexed · bike · binary · binoculars · biohazard · bird · birdhouse · bitcoin · blend · blender · blinds · blocks · bluetooth · bluetooth-connected · bluetooth-off · bluetooth-searching · bold · bolt · bomb · bone · bone-fracture · book · book-a · book-alert · book-audio · book-check · book-copy · book-dashed · book-down · book-headphones · book-heart · book-image · book-key · book-lock · book-marked · book-minus · book-open · book-open-check · book-open-text · book-plus · book-search · book-text · book-type · book-up · book-up-2 · book-user · book-x · bookmark · bookmark-check · bookmark-minus · bookmark-off · bookmark-plus · bookmark-x · boom-box · bot · bot-message-square · bot-off · bottle-wine · bow-arrow · box · boxes · braces · brackets · brain · brain-circuit · brain-cog · brick-wall · brick-wall-fire · brick-wall-shield · briefcase · briefcase-business · briefcase-conveyor-belt · briefcase-medical · bring-to-front · broccoli · brush · brush-cleaning · bubbles · bug · bug-off · bug-play · building · building-2 · bus · bus-front · cable · cable-car · cake · cake-slice · calculator · calendar · calendar-1 · calendar-arrow-down · calendar-arrow-up · calendar-check · calendar-check-2 · calendar-clock · calendar-cog · calendar-days · calendar-fold · calendar-heart · calendar-minus · calendar-minus-2 · calendar-off · calendar-plus · calendar-plus-2 · calendar-range · calendar-search · calendar-sync · calendar-x · calendar-x-2 · calendars · camera · camera-off · candy · candy-cane · candy-off · cannabis · cannabis-off · captions · captions-off · car · car-front · car-taxi-front · caravan · card-sim · carrot · case-lower · case-sensitive · case-upper · cassette-tape · cast · castle · cat · cctv · cctv-off · chart-area · chart-bar · chart-bar-big · chart-bar-decreasing · chart-bar-increasing · chart-bar-stacked · chart-candlestick · chart-column · chart-column-big · chart-column-decreasing · chart-column-increasing · chart-column-stacked · chart-gantt · chart-line · chart-network · chart-no-axes-column · chart-no-axes-column-decreasing · chart-no-axes-column-increasing · chart-no-axes-combined · chart-no-axes-gantt · chart-pie · chart-scatter · chart-spline · check · check-check · check-line · chef-hat · cherry · chess-bishop · chess-king · chess-knight · chess-pawn · chess-queen · chess-rook · chevron-down · chevron-first · chevron-last · chevron-left · chevron-right · chevron-up · chevrons-down · chevrons-down-up · chevrons-left · chevrons-left-right · chevrons-left-right-ellipsis · chevrons-right · chevrons-right-left · chevrons-up · chevrons-up-down · church · cigarette · cigarette-off · circle · circle-alert · circle-arrow-down · circle-arrow-left · circle-arrow-out-down-left · circle-arrow-out-down-right · circle-arrow-out-up-left · circle-arrow-out-up-right · circle-arrow-right · circle-arrow-up · circle-check · circle-check-big · circle-chevron-down · circle-chevron-left · circle-chevron-right · circle-chevron-up · circle-dashed · circle-divide · circle-dollar-sign · circle-dot · circle-dot-dashed · circle-ellipsis · circle-equal · circle-euro · circle-fading-arrow-up · circle-fading-plus · circle-gauge · circle-minus · circle-off · circle-parking · circle-parking-off · circle-pause · circle-percent · circle-pile · circle-play · circle-plus · circle-pound-sterling · circle-power · circle-question-mark · circle-slash · circle-slash-2 · circle-small · circle-star · circle-stop · circle-user · circle-user-round · circle-x · circuit-board · citrus · clapperboard · clipboard · clipboard-check · clipboard-clock · clipboard-copy · clipboard-list · clipboard-minus · clipboard-paste · clipboard-pen · clipboard-pen-line · clipboard-plus · clipboard-type · clipboard-x · clock · clock-1 · clock-10 · clock-11 · clock-12 · clock-2 · clock-3 · clock-4 · clock-5 · clock-6 · clock-7 · clock-8 · clock-9 · clock-alert · clock-arrow-down · clock-arrow-left · clock-arrow-right · clock-arrow-up · clock-check · clock-fading · clock-plus · closed-caption · cloud · cloud-alert · cloud-backup · cloud-check · cloud-cog · cloud-download · cloud-drizzle · cloud-fog · cloud-hail · cloud-lightning · cloud-moon · cloud-moon-rain · cloud-off · cloud-rain · cloud-rain-wind · cloud-snow · cloud-sun · cloud-sun-rain · cloud-sync · cloud-upload · cloudy · clover · club · code · code-xml · coffee · cog · coins · columns-2 · columns-3 · columns-3-cog · columns-4 · combine · command · compass · component · computer · concierge-bell · cone · construction · contact · contact-round · container · contrast · cookie · cooking-pot · copy · copy-check · copy-minus · copy-plus · copy-slash · copy-x · copyleft · copyright · corner-down-left · corner-down-right · corner-left-down · corner-left-up · corner-right-down · corner-right-up · corner-up-left · corner-up-right · cpu · creative-commons · credit-card · croissant · crop · cross · crosshair · crown · cuboid · cup-soda · currency · cylinder · dam · database · database-arrow-down · database-arrow-up · database-backup · database-check · database-minus · database-plus · database-search · database-x · database-zap · decimals-arrow-left · decimals-arrow-right · delete · dessert · diameter · diamond · diamond-minus · diamond-percent · diamond-plus · dice-1 · dice-2 · dice-3 · dice-4 · dice-5 · dice-6 · dices · diff · disc · disc-2 · disc-3 · disc-album · divide · dna · dna-off · dock · dog · dollar-sign · donut · door-closed · door-closed-locked · door-open · dot · download · drafting-compass · drama · drill · drone · droplet · droplet-off · droplets · drum · drumstick · dumbbell · ear · ear-off · earth · earth-lock · eclipse · egg · egg-fried · egg-off · ellipse · ellipsis · ellipsis-vertical · equal · equal-approximately · equal-not · eraser · ethernet-port · euro · ev-charger · expand · external-link · eye · eye-closed · eye-dashed · eye-off · factory · fan · fast-forward · feather · fence · ferris-wheel · file · file-archive · file-axis-3d · file-badge · file-box · file-braces · file-braces-corner · file-chart-column · file-chart-column-increasing · file-chart-line · file-chart-pie · file-check · file-check-corner · file-clock · file-code · file-code-corner · file-cog · file-diff · file-digit · file-down · file-exclamation-point · file-headphone · file-heart · file-image · file-input · file-key · file-lock · file-minus · file-minus-corner · file-music · file-output · file-pen · file-pen-line · file-play · file-plus · file-plus-corner · file-question-mark · file-scan · file-search · file-search-corner · file-signal · file-sliders · file-spreadsheet · file-stack · file-symlink · file-terminal · file-text · file-type · file-type-corner · file-up · file-user · file-video-camera · file-volume · file-x · file-x-corner · files · film · fingerprint-pattern · fire-extinguisher · fish · fish-off · fish-symbol · fishing-hook · fishing-rod · flag · flag-off · flag-triangle-left · flag-triangle-right · flame · flame-kindling · flashlight · flashlight-off · flask-conical · flask-conical-off · flask-round · flip-horizontal-2 · flip-vertical-2 · flower · flower-2 · focus · fold-horizontal · fold-vertical · folder · folder-archive · folder-bookmark · folder-check · folder-clock · folder-closed · folder-code · folder-cog · folder-dot · folder-down · folder-git · folder-git-2 · folder-heart · folder-input · folder-kanban · folder-key · folder-lock · folder-minus · folder-open · folder-open-dot · folder-output · folder-pen · folder-plus · folder-root · folder-search · folder-search-2 · folder-symlink · folder-sync · folder-tree · folder-up · folder-x · folders · footprints · forklift · form · forward · frame · frown · fuel · fullscreen · funnel · funnel-plus · funnel-x · gallery-horizontal · gallery-horizontal-end · gallery-thumbnails · gallery-vertical · gallery-vertical-end · gamepad · gamepad-2 · gamepad-directional · gauge · gavel · gem · georgian-lari · ghost · gift · git-branch · git-branch-minus · git-branch-plus · git-commit-horizontal · git-commit-vertical · git-compare · git-compare-arrows · git-fork · git-graph · git-merge · git-merge-conflict · git-pull-request · git-pull-request-arrow · git-pull-request-closed · git-pull-request-create · git-pull-request-create-arrow · git-pull-request-draft · glass-water · glasses · globe · globe-check · globe-lock · globe-off · globe-x · goal · gpu · graduation-cap · grape · grid-2x2 · grid-2x2-check · grid-2x2-plus · grid-2x2-x · grid-3x2 · grid-3x3 · grip · grip-horizontal · grip-vertical · group · guitar · ham · hamburger · hammer · hand · hand-coins · hand-fist · hand-grab · hand-heart · hand-helping · hand-metal · hand-platter · handbag · handshake · hard-drive · hard-drive-download · hard-drive-upload · hard-hat · hash · hat-glasses · haze · hd · hdmi-port · heading · heading-1 · heading-2 · heading-3 · heading-4 · heading-5 · heading-6 · headphone-off · headphones · headset · heart · heart-crack · heart-handshake · heart-minus · heart-off · heart-plus · heart-pulse · heart-x · heater · helicopter · hexagon · highlighter · hop · hop-off · hospital · hotel · hourglass · house · house-heart · house-plug · house-plus · house-wifi · ice-cream-bowl · ice-cream-cone · id-card · id-card-lanyard · image · image-down · image-minus · image-off · image-play · image-plus · image-up · image-upscale · images · import · inbox · indian-rupee · infinity · info · inspection-panel · italic · iteration-ccw · iteration-cw · japanese-yen · joystick · kanban · kayak · key · key-round · key-square · keyboard · keyboard-music · keyboard-off · lamp · lamp-ceiling · lamp-desk · lamp-floor · lamp-wall-down · lamp-wall-up · land-plot · landmark · languages · laptop · laptop-minimal · laptop-minimal-check · lasso · lasso-select · laugh · layers · layers-2 · layers-minus · layers-plus · layout-dashboard · layout-freeform · layout-grid · layout-list · layout-panel-left · layout-panel-top · layout-template · leaf · leafy-green · lectern · lens-concave · lens-convex · library · library-big · life-buoy · ligature · lightbulb · lightbulb-off · line-dot-right-horizontal · line-squiggle · line-style · link · link-2 · link-2-off · list · list-check · list-checks · list-chevrons-down-up · list-chevrons-up-down · list-collapse · list-end · list-filter · list-filter-plus · list-indent-decrease · list-indent-increase · list-minus · list-music · list-ordered · list-plus · list-restart · list-sort-ascending · list-sort-descending · list-start · list-todo · list-tree · list-video · list-x · loader · loader-circle · loader-pinwheel · locate · locate-fixed · locate-off · lock · lock-keyhole · lock-keyhole-open · lock-open · log-in · log-out · logs · lollipop · luggage · magnet · mail · mail-check · mail-minus · mail-open · mail-plus · mail-question-mark · mail-search · mail-warning · mail-x · mailbox · mails · map · map-minus · map-pin · map-pin-check · map-pin-check-inside · map-pin-house · map-pin-minus · map-pin-minus-inside · map-pin-off · map-pin-pen · map-pin-plus · map-pin-plus-inside · map-pin-search · map-pin-x · map-pin-x-inside · map-pinned · map-plus · mars · mars-stroke · martini · maximize · maximize-2 · medal · megaphone · megaphone-off · meh · memory-stick · menu · merge · message-circle · message-circle-check · message-circle-code · message-circle-dashed · message-circle-heart · message-circle-more · message-circle-off · message-circle-plus · message-circle-question-mark · message-circle-reply · message-circle-warning · message-circle-x · message-square · message-square-check · message-square-code · message-square-dashed · message-square-diff · message-square-dot · message-square-heart · message-square-lock · message-square-more · message-square-off · message-square-plus · message-square-quote · message-square-reply · message-square-share · message-square-text · message-square-warning · message-square-x · messages-square · metronome · mic · mic-audio-lines · mic-off · mic-signal · mic-vocal · microchip · microscope · microwave · milestone · milk · milk-off · minimize · minimize-2 · minus · mirror-rectangular · mirror-round · monitor · monitor-check · monitor-cloud · monitor-cog · monitor-dot · monitor-down · monitor-off · monitor-pause · monitor-play · monitor-smartphone · monitor-speaker · monitor-stop · monitor-up · monitor-x · moon · moon-star · mosque · motorbike · mountain · mountain-snow · mouse · mouse-left · mouse-off · mouse-pointer · mouse-pointer-2 · mouse-pointer-2-off · mouse-pointer-ban · mouse-pointer-click · mouse-right · move · move-3d · move-diagonal · move-diagonal-2 · move-down · move-down-left · move-down-right · move-horizontal · move-left · move-right · move-up · move-up-left · move-up-right · move-vertical · music · music-2 · music-3 · music-4 · navigation · navigation-2 · navigation-2-off · navigation-off · network · newspaper · nfc · non-binary · notebook · notebook-pen · notebook-tabs · notebook-text · notepad-text · notepad-text-dashed · nut · nut-off · octagon · octagon-alert · octagon-minus · octagon-pause · octagon-x · omega · option · orbit · origami · package · package-2 · package-check · package-minus · package-open · package-plus · package-search · package-x · paint-bucket · paint-roller · paintbrush · paintbrush-vertical · palette · panda · panel-bottom · panel-bottom-close · panel-bottom-dashed · panel-bottom-open · panel-left · panel-left-close · panel-left-dashed · panel-left-open · panel-left-right-dashed · panel-right · panel-right-close · panel-right-dashed · panel-right-open · panel-top · panel-top-bottom-dashed · panel-top-close · panel-top-dashed · panel-top-open · panels-left-bottom · panels-right-bottom · panels-top-left · paper-bag · paperclip · parasol · parentheses · parking-meter · party-popper · pause · paw-print · pc-case · pen · pen-line · pen-off · pen-tool · pencil · pencil-line · pencil-off · pencil-ruler · pencil-sparkles · pentagon · percent · person-standing · phi · philippine-peso · phone · phone-call · phone-forwarded · phone-incoming · phone-missed · phone-off · phone-outgoing · pi · piano · pickaxe · picture-in-picture · picture-in-picture-2 · piggy-bank · pilcrow · pilcrow-left · pilcrow-right · pill · pill-bottle · pin · pin-off · pipette · pizza · plane · plane-landing · plane-takeoff · play · play-off · plug · plug-2 · plug-zap · plus · pocket-knife · podium · pointer · pointer-off · popcorn · popsicle · pound-sterling · power · power-off · presentation · printer · printer-check · printer-x · projector · proportions · puzzle · pyramid · qr-code · quote · rabbit · radar · radiation · radical · radio · radio-off · radio-receiver · radio-tower · radius · rainbow · rat · ratio · receipt · receipt-cent · receipt-euro · receipt-indian-rupee · receipt-japanese-yen · receipt-pound-sterling · receipt-russian-ruble · receipt-swiss-franc · receipt-text · receipt-turkish-lira · rectangle-circle · rectangle-ellipsis · rectangle-goggles · rectangle-horizontal · rectangle-vertical · recycle · redo · redo-2 · redo-dot · refresh-ccw · refresh-ccw-dot · refresh-cw · refresh-cw-off · refrigerator · regex · remove-formatting · repeat · repeat-1 · repeat-2 · repeat-off · replace · replace-all · reply · reply-all · rewind · ribbon · road · rocket · rocking-chair · roller-coaster · rose · rotate-3d · rotate-ccw · rotate-ccw-clock · rotate-ccw-key · rotate-ccw-square · rotate-cw · rotate-cw-fading-clock · rotate-cw-square · route · route-off · router · rows-2 · rows-3 · rows-4 · rss · ruler · ruler-dimension-line · russian-ruble · sailboat · salad · sandwich · satellite · satellite-dish · saudi-riyal · save · save-all · save-check · save-off · save-pen · save-plus · scale · scale-3d · scaling · scan · scan-barcode · scan-box · scan-eye · scan-face · scan-heart · scan-line · scan-qr-code · scan-search · scan-square · scan-text · school · scissors · scissors-line-dashed · scooter · screen-share · screen-share-off · scroll · scroll-text · search · search-alert · search-check · search-code · search-slash · search-x · section · send · send-horizontal · send-to-back · separator-horizontal · separator-vertical · server · server-cog · server-crash · server-off · server-plus · settings · settings-2 · shapes · share · share-2 · sheet · shell · shelving-unit · shield · shield-alert · shield-ban · shield-check · shield-cog · shield-cog-corner · shield-ellipsis · shield-half · shield-keyhole · shield-minus · shield-off · shield-plus · shield-question-mark · shield-user · shield-x · ship · ship-wheel · shirt · shopping-bag · shopping-basket · shopping-cart · shovel · shower-head · shredder · shrimp · shrink · shrub · shuffle · sigma · signal · signal-high · signal-low · signal-medium · signal-zero · signature · signpost · signpost-big · siren · skip-back · skip-forward · skull · slash · slice · sliders-horizontal · sliders-vertical · smartphone · smartphone-charging · smartphone-nfc · smile · smile-plus · snail · snowflake · soap-dispenser-droplet · sofa · solar-panel · soup · space · spade · sparkle · sparkles · speaker · speech · spell-check · spell-check-2 · spline · spline-pointer · split · spool · sport-shoe · spotlight · spray-can · sprout · square · square-activity · square-arrow-down · square-arrow-down-left · square-arrow-down-right · square-arrow-left · square-arrow-out-down-left · square-arrow-out-down-right · square-arrow-out-up-left · square-arrow-out-up-right · square-arrow-right · square-arrow-right-enter · square-arrow-right-exit · square-arrow-up · square-arrow-up-left · square-arrow-up-right · square-asterisk · square-bottom-dashed-scissors · square-centerline-dashed-horizontal · square-centerline-dashed-vertical · square-chart-gantt · square-check · square-check-big · square-chevron-down · square-chevron-left · square-chevron-right · square-chevron-up · square-code · square-dashed · square-dashed-bottom · square-dashed-bottom-code · square-dashed-kanban · square-dashed-mouse-pointer · square-dashed-text · square-dashed-top-solid · square-divide · square-dot · square-equal · square-function · square-kanban · square-library · square-m · square-menu · square-minus · square-mouse-pointer · square-off · square-parking · square-parking-off · square-pause · square-pen · square-percent · square-pi · square-pilcrow · square-play · square-plus · square-power · square-radical · square-round-corner · square-scissors · square-sigma · square-slash · square-split-horizontal · square-split-vertical · square-square · square-stack · square-star · square-stop · square-terminal · square-user · square-user-round · square-x · squares-exclude · squares-intersect · squares-subtract · squares-unite · squircle · squircle-dashed · squirrel · stamp · star · star-check · star-half · star-minus · star-off · star-plus · star-x · step-back · step-forward · stethoscope · sticker · sticky-note · sticky-note-check · sticky-note-minus · sticky-note-off · sticky-note-plus · sticky-note-x · sticky-notes · stone · store · stretch-horizontal · stretch-vertical · strikethrough · subscript · summary · sun · sun-dim · sun-medium · sun-moon · sun-snow · sunrise · sunset · superscript · swatch-book · swiss-franc · switch-camera · sword · swords · syringe · table · table-2 · table-cells-merge · table-cells-split · table-columns-split · table-of-contents · table-properties · table-rows-split · tablet · tablet-smartphone · tablets · tag · tag-plus · tag-x · tags · tally-1 · tally-2 · tally-3 · tally-4 · tally-5 · tangent · target · telescope · tent · tent-tree · terminal · test-tube · test-tube-diagonal · test-tubes · text-align-center · text-align-end · text-align-justify · text-align-start · text-cursor · text-cursor-input · text-initial · text-quote · text-search · text-wrap · theater · thermometer · thermometer-snowflake · thermometer-sun · thumbs-down · thumbs-up · ticket · ticket-check · ticket-minus · ticket-percent · ticket-plus · ticket-slash · ticket-x · tickets · tickets-plane · timeline · timer · timer-off · timer-reset · toggle-left · toggle-right · toilet · tool-case · toolbox · tornado · torus · touchpad · touchpad-off · towel-rack · tower-control · toy-brick · tractor · traffic-cone · train-front · train-front-tunnel · train-track · tram-front · transgender · trash · trash-2 · tree-deciduous · tree-palm · tree-pine · trees · trending-down · trending-up · trending-up-down · triangle · triangle-alert · triangle-dashed · triangle-right · trophy · truck · truck-electric · turkish-lira · turntable · turtle · tv · tv-minimal · tv-minimal-play · type · type-outline · umbrella · umbrella-off · underline · undo · undo-2 · undo-dot · unfold-horizontal · unfold-vertical · ungroup · university · unlink · unlink-2 · unplug · upload · usb · user · user-check · user-cog · user-key · user-lock · user-minus · user-pen · user-plus · user-round · user-round-arrow-left · user-round-check · user-round-cog · user-round-key · user-round-minus · user-round-pen · user-round-plus · user-round-search · user-round-x · user-search · user-shield · user-star · user-x · users · users-round · utensils · utensils-crossed · utility-pole · van · variable · vault · vector-square · vegan · venetian-mask · venus · venus-and-mars · vibrate · vibrate-off · video · video-off · videotape · view · voicemail · volleyball · volume · volume-1 · volume-2 · volume-off · volume-x · vote · wallet · wallet-cards · wallet-minimal · wallpaper · wand · wand-sparkles · warehouse · washing-machine · watch · waves-arrow-down · waves-arrow-up · waves-horizontal · waves-ladder · waves-vertical · waypoints · webcam · webcam-off · webhook · webhook-off · weight · weight-tilde · wheat · wheat-off · whole-word · wifi · wifi-cog · wifi-high · wifi-low · wifi-off · wifi-pen · wifi-sync · wifi-zero · wind · wind-arrow-down · wine · wine-off · workflow · worm · wrench · wrench-off · x · x-line-top · zap · zap-off · zodiac-aquarius · zodiac-aries · zodiac-cancer · zodiac-capricorn · zodiac-gemini · zodiac-leo · zodiac-libra · zodiac-ophiuchus · zodiac-pisces · zodiac-sagittarius · zodiac-scorpio · zodiac-taurus · zodiac-virgo · zoom-in · zoom-out
Heroicons — Tailwind’s UI icon set · 324 · MIT
asset:svg/heroicons/<name>.svg
academic-cap · adjustments-horizontal · adjustments-vertical · archive-box · archive-box-arrow-down · archive-box-x-mark · arrow-down · arrow-down-circle · arrow-down-left · arrow-down-on-square · arrow-down-on-square-stack · arrow-down-right · arrow-down-tray · arrow-left · arrow-left-circle · arrow-left-end-on-rectangle · arrow-left-on-rectangle · arrow-left-start-on-rectangle · arrow-long-down · arrow-long-left · arrow-long-right · arrow-long-up · arrow-path · arrow-path-rounded-square · arrow-right · arrow-right-circle · arrow-right-end-on-rectangle · arrow-right-on-rectangle · arrow-right-start-on-rectangle · arrow-small-down · arrow-small-left · arrow-small-right · arrow-small-up · arrow-top-right-on-square · arrow-trending-down · arrow-trending-up · arrow-turn-down-left · arrow-turn-down-right · arrow-turn-left-down · arrow-turn-left-up · arrow-turn-right-down · arrow-turn-right-up · arrow-turn-up-left · arrow-turn-up-right · arrow-up · arrow-up-circle · arrow-up-left · arrow-up-on-square · arrow-up-on-square-stack · arrow-up-right · arrow-up-tray · arrow-uturn-down · arrow-uturn-left · arrow-uturn-right · arrow-uturn-up · arrows-pointing-in · arrows-pointing-out · arrows-right-left · arrows-up-down · at-symbol · backspace · backward · banknotes · bars-2 · bars-3 · bars-3-bottom-left · bars-3-bottom-right · bars-3-center-left · bars-4 · bars-arrow-down · bars-arrow-up · battery-0 · battery-100 · battery-50 · beaker · bell · bell-alert · bell-slash · bell-snooze · bold · bolt · bolt-slash · book-open · bookmark · bookmark-slash · bookmark-square · briefcase · bug-ant · building-library · building-office · building-office-2 · building-storefront · cake · calculator · calendar · calendar-date-range · calendar-days · camera · chart-bar · chart-bar-square · chart-pie · chat-bubble-bottom-center · chat-bubble-bottom-center-text · chat-bubble-left · chat-bubble-left-ellipsis · chat-bubble-left-right · chat-bubble-oval-left · chat-bubble-oval-left-ellipsis · check · check-badge · check-circle · chevron-double-down · chevron-double-left · chevron-double-right · chevron-double-up · chevron-down · chevron-left · chevron-right · chevron-up · chevron-up-down · circle-stack · clipboard · clipboard-document · clipboard-document-check · clipboard-document-list · clock · cloud · cloud-arrow-down · cloud-arrow-up · code-bracket · code-bracket-square · cog · cog-6-tooth · cog-8-tooth · command-line · computer-desktop · cpu-chip · credit-card · cube · cube-transparent · currency-bangladeshi · currency-dollar · currency-euro · currency-pound · currency-rupee · currency-yen · cursor-arrow-rays · cursor-arrow-ripple · device-phone-mobile · device-tablet · divide · document · document-arrow-down · document-arrow-up · document-chart-bar · document-check · document-currency-bangladeshi · document-currency-dollar · document-currency-euro · document-currency-pound · document-currency-rupee · document-currency-yen · document-duplicate · document-magnifying-glass · document-minus · document-plus · document-text · ellipsis-horizontal · ellipsis-horizontal-circle · ellipsis-vertical · envelope · envelope-open · equals · exclamation-circle · exclamation-triangle · eye · eye-dropper · eye-slash · face-frown · face-smile · film · finger-print · fire · flag · folder · folder-arrow-down · folder-minus · folder-open · folder-plus · forward · funnel · gif · gift · gift-top · globe-alt · globe-americas · globe-asia-australia · globe-europe-africa · h1 · h2 · h3 · hand-raised · hand-thumb-down · hand-thumb-up · hashtag · heart · home · home-modern · identification · inbox · inbox-arrow-down · inbox-stack · information-circle · italic · key · language · lifebuoy · light-bulb · link · link-slash · list-bullet · lock-closed · lock-open · magnifying-glass · magnifying-glass-circle · magnifying-glass-minus · magnifying-glass-plus · map · map-pin · megaphone · microphone · minus · minus-circle · minus-small · moon · musical-note · newspaper · no-symbol · numbered-list · paint-brush · paper-airplane · paper-clip · pause · pause-circle · pencil · pencil-square · percent-badge · phone · phone-arrow-down-left · phone-arrow-up-right · phone-x-mark · photo · play · play-circle · play-pause · plus · plus-circle · plus-small · power · presentation-chart-bar · presentation-chart-line · printer · puzzle-piece · qr-code · question-mark-circle · queue-list · radio · receipt-percent · receipt-refund · rectangle-group · rectangle-stack · rocket-launch · rss · scale · scissors · server · server-stack · share · shield-check · shield-exclamation · shopping-bag · shopping-cart · signal · signal-slash · slash · sparkles · speaker-wave · speaker-x-mark · square-2-stack · square-3-stack-3d · squares-2x2 · squares-plus · star · stop · stop-circle · strikethrough · sun · swatch · table-cells · tag · ticket · trash · trophy · truck · tv · underline · user · user-circle · user-group · user-minus · user-plus · users · variable · video-camera · video-camera-slash · view-columns · viewfinder-circle · wallet · wifi · window · wrench · wrench-screwdriver · x-circle · x-mark
Flags — country flags, two-letter ISO 3166-1 codes · 271 · MIT
asset:svg/flags/<name>.svg
ad · ae · af · ag · ai · al · am · ao · aq · ar · arab · as · asean · at · au · aw · ax · az · ba · bb · bd · be · bf · bg · bh · bi · bj · bl · bm · bn · bo · bq · br · bs · bt · bv · bw · by · bz · ca · cc · cd · cefta · cf · cg · ch · ci · ck · cl · cm · cn · co · cp · cr · cu · cv · cw · cx · cy · cz · de · dg · dj · dk · dm · do · dz · eac · ec · ee · eg · eh · er · es · es-ct · es-ga · es-pv · et · eu · fi · fj · fk · fm · fo · fr · ga · gb · gb-eng · gb-nir · gb-sct · gb-wls · gd · ge · gf · gg · gh · gi · gl · gm · gn · gp · gq · gr · gs · gt · gu · gw · gy · hk · hm · hn · hr · ht · hu · ic · id · ie · il · im · in · io · iq · ir · is · it · je · jm · jo · jp · ke · kg · kh · ki · km · kn · kp · kr · kw · ky · kz · la · lb · lc · li · lk · lr · ls · lt · lu · lv · ly · ma · mc · md · me · mf · mg · mh · mk · ml · mm · mn · mo · mp · mq · mr · ms · mt · mu · mv · mw · mx · my · mz · na · nc · ne · nf · ng · ni · nl · no · np · nr · nu · nz · om · pa · pc · pe · pf · pg · ph · pk · pl · pm · pn · pr · ps · pt · pw · py · qa · re · ro · rs · ru · rw · sa · sb · sc · sd · se · sg · sh · sh-ac · sh-hl · sh-ta · si · sj · sk · sl · sm · sn · so · sr · ss · st · sv · sx · sy · sz · tc · td · tf · tg · th · tj · tk · tl · tm · tn · to · tr · tt · tv · tw · tz · ua · ug · um · un · us · uy · uz · va · vc · ve · vg · vi · vn · vu · wf · ws · xk · xx · ye · yt · za · zm · zw
Emoji — Twemoji — full-colour emoji · 4,073 · CC-BY 4.0
asset:svg/emoji/<name>.svg
Friendly aliases (64) — plain-English names, the easy way in:
bell · brain · bulb · car · chart · check · clap · cloud · comet · computer · crescentmoon · cross · door · earth · eyes · fire · firstquarter · flag · fullmoon · glowstar · goat · grinning · heart · hourglass · joy · jupiter · key · lastquarter · lock · mail · mars · mercury · milkyway · money · moon · muscle · neptune · newmoon · party · phone · rainbow · rocket · saturn · smile · sob · star · sun · sunface · target · telescope · thinking · thumbsdown · thumbsup · trophy · uranus · venus · waningcrescent · waninggibbous · warning · wave · waxingcrescent · waxinggibbous · wink · zap
All emoji (4,009) are also addressable by their Unicode codepoint filename (e.g. 1f680 = 🚀). Expand to browse every one:
Show all 4,009 emoji codepoints
1f004 · 1f0cf · 1f170 · 1f171 · 1f17e · 1f17f · 1f18e · 1f191 · 1f192 · 1f193 · 1f194 · 1f195 · 1f196 · 1f197 · 1f198 · 1f199 · 1f19a · 1f1e6 · 1f1e6-1f1e8 · 1f1e6-1f1e9 · 1f1e6-1f1ea · 1f1e6-1f1eb · 1f1e6-1f1ec · 1f1e6-1f1ee · 1f1e6-1f1f1 · 1f1e6-1f1f2 · 1f1e6-1f1f4 · 1f1e6-1f1f6 · 1f1e6-1f1f7 · 1f1e6-1f1f8 · 1f1e6-1f1f9 · 1f1e6-1f1fa · 1f1e6-1f1fc · 1f1e6-1f1fd · 1f1e6-1f1ff · 1f1e7 · 1f1e7-1f1e6 · 1f1e7-1f1e7 · 1f1e7-1f1e9 · 1f1e7-1f1ea · 1f1e7-1f1eb · 1f1e7-1f1ec · 1f1e7-1f1ed · 1f1e7-1f1ee · 1f1e7-1f1ef · 1f1e7-1f1f1 · 1f1e7-1f1f2 · 1f1e7-1f1f3 · 1f1e7-1f1f4 · 1f1e7-1f1f6 · 1f1e7-1f1f7 · 1f1e7-1f1f8 · 1f1e7-1f1f9 · 1f1e7-1f1fb · 1f1e7-1f1fc · 1f1e7-1f1fe · 1f1e7-1f1ff · 1f1e8 · 1f1e8-1f1e6 · 1f1e8-1f1e8 · 1f1e8-1f1e9 · 1f1e8-1f1eb · 1f1e8-1f1ec · 1f1e8-1f1ed · 1f1e8-1f1ee · 1f1e8-1f1f0 · 1f1e8-1f1f1 · 1f1e8-1f1f2 · 1f1e8-1f1f3 · 1f1e8-1f1f4 · 1f1e8-1f1f5 · 1f1e8-1f1f6 · 1f1e8-1f1f7 · 1f1e8-1f1fa · 1f1e8-1f1fb · 1f1e8-1f1fc · 1f1e8-1f1fd · 1f1e8-1f1fe · 1f1e8-1f1ff · 1f1e9 · 1f1e9-1f1ea · 1f1e9-1f1ec · 1f1e9-1f1ef · 1f1e9-1f1f0 · 1f1e9-1f1f2 · 1f1e9-1f1f4 · 1f1e9-1f1ff · 1f1ea · 1f1ea-1f1e6 · 1f1ea-1f1e8 · 1f1ea-1f1ea · 1f1ea-1f1ec · 1f1ea-1f1ed · 1f1ea-1f1f7 · 1f1ea-1f1f8 · 1f1ea-1f1f9 · 1f1ea-1f1fa · 1f1eb · 1f1eb-1f1ee · 1f1eb-1f1ef · 1f1eb-1f1f0 · 1f1eb-1f1f2 · 1f1eb-1f1f4 · 1f1eb-1f1f7 · 1f1ec · 1f1ec-1f1e6 · 1f1ec-1f1e7 · 1f1ec-1f1e9 · 1f1ec-1f1ea · 1f1ec-1f1eb · 1f1ec-1f1ec · 1f1ec-1f1ed · 1f1ec-1f1ee · 1f1ec-1f1f1 · 1f1ec-1f1f2 · 1f1ec-1f1f3 · 1f1ec-1f1f5 · 1f1ec-1f1f6 · 1f1ec-1f1f7 · 1f1ec-1f1f8 · 1f1ec-1f1f9 · 1f1ec-1f1fa · 1f1ec-1f1fc · 1f1ec-1f1fe · 1f1ed · 1f1ed-1f1f0 · 1f1ed-1f1f2 · 1f1ed-1f1f3 · 1f1ed-1f1f7 · 1f1ed-1f1f9 · 1f1ed-1f1fa · 1f1ee · 1f1ee-1f1e8 · 1f1ee-1f1e9 · 1f1ee-1f1ea · 1f1ee-1f1f1 · 1f1ee-1f1f2 · 1f1ee-1f1f3 · 1f1ee-1f1f4 · 1f1ee-1f1f6 · 1f1ee-1f1f7 · 1f1ee-1f1f8 · 1f1ee-1f1f9 · 1f1ef · 1f1ef-1f1ea · 1f1ef-1f1f2 · 1f1ef-1f1f4 · 1f1ef-1f1f5 · 1f1f0 · 1f1f0-1f1ea · 1f1f0-1f1ec · 1f1f0-1f1ed · 1f1f0-1f1ee · 1f1f0-1f1f2 · 1f1f0-1f1f3 · 1f1f0-1f1f5 · 1f1f0-1f1f7 · 1f1f0-1f1fc · 1f1f0-1f1fe · 1f1f0-1f1ff · 1f1f1 · 1f1f1-1f1e6 · 1f1f1-1f1e7 · 1f1f1-1f1e8 · 1f1f1-1f1ee · 1f1f1-1f1f0 · 1f1f1-1f1f7 · 1f1f1-1f1f8 · 1f1f1-1f1f9 · 1f1f1-1f1fa · 1f1f1-1f1fb · 1f1f1-1f1fe · 1f1f2 · 1f1f2-1f1e6 · 1f1f2-1f1e8 · 1f1f2-1f1e9 · 1f1f2-1f1ea · 1f1f2-1f1eb · 1f1f2-1f1ec · 1f1f2-1f1ed · 1f1f2-1f1f0 · 1f1f2-1f1f1 · 1f1f2-1f1f2 · 1f1f2-1f1f3 · 1f1f2-1f1f4 · 1f1f2-1f1f5 · 1f1f2-1f1f6 · 1f1f2-1f1f7 · 1f1f2-1f1f8 · 1f1f2-1f1f9 · 1f1f2-1f1fa · 1f1f2-1f1fb · 1f1f2-1f1fc · 1f1f2-1f1fd · 1f1f2-1f1fe · 1f1f2-1f1ff · 1f1f3 · 1f1f3-1f1e6 · 1f1f3-1f1e8 · 1f1f3-1f1ea · 1f1f3-1f1eb · 1f1f3-1f1ec · 1f1f3-1f1ee · 1f1f3-1f1f1 · 1f1f3-1f1f4 · 1f1f3-1f1f5 · 1f1f3-1f1f7 · 1f1f3-1f1fa · 1f1f3-1f1ff · 1f1f4 · 1f1f4-1f1f2 · 1f1f5 · 1f1f5-1f1e6 · 1f1f5-1f1ea · 1f1f5-1f1eb · 1f1f5-1f1ec · 1f1f5-1f1ed · 1f1f5-1f1f0 · 1f1f5-1f1f1 · 1f1f5-1f1f2 · 1f1f5-1f1f3 · 1f1f5-1f1f7 · 1f1f5-1f1f8 · 1f1f5-1f1f9 · 1f1f5-1f1fc · 1f1f5-1f1fe · 1f1f6 · 1f1f6-1f1e6 · 1f1f7 · 1f1f7-1f1ea · 1f1f7-1f1f4 · 1f1f7-1f1f8 · 1f1f7-1f1fa · 1f1f7-1f1fc · 1f1f8 · 1f1f8-1f1e6 · 1f1f8-1f1e7 · 1f1f8-1f1e8 · 1f1f8-1f1e9 · 1f1f8-1f1ea · 1f1f8-1f1ec · 1f1f8-1f1ed · 1f1f8-1f1ee · 1f1f8-1f1ef · 1f1f8-1f1f0 · 1f1f8-1f1f1 · 1f1f8-1f1f2 · 1f1f8-1f1f3 · 1f1f8-1f1f4 · 1f1f8-1f1f7 · 1f1f8-1f1f8 · 1f1f8-1f1f9 · 1f1f8-1f1fb · 1f1f8-1f1fd · 1f1f8-1f1fe · 1f1f8-1f1ff · 1f1f9 · 1f1f9-1f1e6 · 1f1f9-1f1e8 · 1f1f9-1f1e9 · 1f1f9-1f1eb · 1f1f9-1f1ec · 1f1f9-1f1ed · 1f1f9-1f1ef · 1f1f9-1f1f0 · 1f1f9-1f1f1 · 1f1f9-1f1f2 · 1f1f9-1f1f3 · 1f1f9-1f1f4 · 1f1f9-1f1f7 · 1f1f9-1f1f9 · 1f1f9-1f1fb · 1f1f9-1f1fc · 1f1f9-1f1ff · 1f1fa · 1f1fa-1f1e6 · 1f1fa-1f1ec · 1f1fa-1f1f2 · 1f1fa-1f1f3 · 1f1fa-1f1f8 · 1f1fa-1f1fe · 1f1fa-1f1ff · 1f1fb · 1f1fb-1f1e6 · 1f1fb-1f1e8 · 1f1fb-1f1ea · 1f1fb-1f1ec · 1f1fb-1f1ee · 1f1fb-1f1f3 · 1f1fb-1f1fa · 1f1fc · 1f1fc-1f1eb · 1f1fc-1f1f8 · 1f1fd · 1f1fd-1f1f0 · 1f1fe · 1f1fe-1f1ea · 1f1fe-1f1f9 · 1f1ff · 1f1ff-1f1e6 · 1f1ff-1f1f2 · 1f1ff-1f1fc · 1f201 · 1f202 · 1f21a · 1f22f · 1f232 · 1f233 · 1f234 · 1f235 · 1f236 · 1f237 · 1f238 · 1f239 · 1f23a · 1f250 · 1f251 · 1f300 · 1f301 · 1f302 · 1f303 · 1f304 · 1f305 · 1f306 · 1f307 · 1f308 · 1f309 · 1f30a · 1f30b · 1f30c · 1f30d · 1f30e · 1f30f · 1f310 · 1f311 · 1f312 · 1f313 · 1f314 · 1f315 · 1f316 · 1f317 · 1f318 · 1f319 · 1f31a · 1f31b · 1f31c · 1f31d · 1f31e · 1f31f · 1f320 · 1f321 · 1f324 · 1f325 · 1f326 · 1f327 · 1f328 · 1f329 · 1f32a · 1f32b · 1f32c · 1f32d · 1f32e · 1f32f · 1f330 · 1f331 · 1f332 · 1f333 · 1f334 · 1f335 · 1f336 · 1f337 · 1f338 · 1f339 · 1f33a · 1f33b · 1f33c · 1f33d · 1f33e · 1f33f · 1f340 · 1f341 · 1f342 · 1f343 · 1f344 · 1f344-200d-1f7eb · 1f345 · 1f346 · 1f347 · 1f348 · 1f349 · 1f34a · 1f34b · 1f34b-200d-1f7e9 · 1f34c · 1f34d · 1f34e · 1f34f · 1f350 · 1f351 · 1f352 · 1f353 · 1f354 · 1f355 · 1f356 · 1f357 · 1f358 · 1f359 · 1f35a · 1f35b · 1f35c · 1f35d · 1f35e · 1f35f · 1f360 · 1f361 · 1f362 · 1f363 · 1f364 · 1f365 · 1f366 · 1f367 · 1f368 · 1f369 · 1f36a · 1f36b · 1f36c · 1f36d · 1f36e · 1f36f · 1f370 · 1f371 · 1f372 · 1f373 · 1f374 · 1f375 · 1f376 · 1f377 · 1f378 · 1f379 · 1f37a · 1f37b · 1f37c · 1f37d · 1f37e · 1f37f · 1f380 · 1f381 · 1f382 · 1f383 · 1f384 · 1f385 · 1f385-1f3fb · 1f385-1f3fc · 1f385-1f3fd · 1f385-1f3fe · 1f385-1f3ff · 1f386 · 1f387 · 1f388 · 1f389 · 1f38a · 1f38b · 1f38c · 1f38d · 1f38e · 1f38f · 1f390 · 1f391 · 1f392 · 1f393 · 1f396 · 1f397 · 1f399 · 1f39a · 1f39b · 1f39e · 1f39f · 1f3a0 · 1f3a1 · 1f3a2 · 1f3a3 · 1f3a4 · 1f3a5 · 1f3a6 · 1f3a7 · 1f3a8 · 1f3a9 · 1f3aa · 1f3ab · 1f3ac · 1f3ad · 1f3ae · 1f3af · 1f3b0 · 1f3b1 · 1f3b2 · 1f3b3 · 1f3b4 · 1f3b5 · 1f3b6 · 1f3b7 · 1f3b8 · 1f3b9 · 1f3ba · 1f3bb · 1f3bc · 1f3bd · 1f3be · 1f3bf · 1f3c0 · 1f3c1 · 1f3c2 · 1f3c2-1f3fb · 1f3c2-1f3fc · 1f3c2-1f3fd · 1f3c2-1f3fe · 1f3c2-1f3ff · 1f3c3 · 1f3c3-1f3fb · 1f3c3-1f3fb-200d-2640-fe0f · 1f3c3-1f3fb-200d-2640-fe0f-200d-27a1-fe0f · 1f3c3-1f3fb-200d-2642-fe0f · 1f3c3-1f3fb-200d-2642-fe0f-200d-27a1-fe0f · 1f3c3-1f3fb-200d-27a1-fe0f · 1f3c3-1f3fc · 1f3c3-1f3fc-200d-2640-fe0f · 1f3c3-1f3fc-200d-2640-fe0f-200d-27a1-fe0f · 1f3c3-1f3fc-200d-2642-fe0f · 1f3c3-1f3fc-200d-2642-fe0f-200d-27a1-fe0f · 1f3c3-1f3fc-200d-27a1-fe0f · 1f3c3-1f3fd · 1f3c3-1f3fd-200d-2640-fe0f · 1f3c3-1f3fd-200d-2640-fe0f-200d-27a1-fe0f · 1f3c3-1f3fd-200d-2642-fe0f · 1f3c3-1f3fd-200d-2642-fe0f-200d-27a1-fe0f · 1f3c3-1f3fd-200d-27a1-fe0f · 1f3c3-1f3fe · 1f3c3-1f3fe-200d-2640-fe0f · 1f3c3-1f3fe-200d-2640-fe0f-200d-27a1-fe0f · 1f3c3-1f3fe-200d-2642-fe0f · 1f3c3-1f3fe-200d-2642-fe0f-200d-27a1-fe0f · 1f3c3-1f3fe-200d-27a1-fe0f · 1f3c3-1f3ff · 1f3c3-1f3ff-200d-2640-fe0f · 1f3c3-1f3ff-200d-2640-fe0f-200d-27a1-fe0f · 1f3c3-1f3ff-200d-2642-fe0f · 1f3c3-1f3ff-200d-2642-fe0f-200d-27a1-fe0f · 1f3c3-1f3ff-200d-27a1-fe0f · 1f3c3-200d-2640-fe0f · 1f3c3-200d-2640-fe0f-200d-27a1-fe0f · 1f3c3-200d-2642-fe0f · 1f3c3-200d-2642-fe0f-200d-27a1-fe0f · 1f3c3-200d-27a1-fe0f · 1f3c4 · 1f3c4-1f3fb · 1f3c4-1f3fb-200d-2640-fe0f · 1f3c4-1f3fb-200d-2642-fe0f · 1f3c4-1f3fc · 1f3c4-1f3fc-200d-2640-fe0f · 1f3c4-1f3fc-200d-2642-fe0f · 1f3c4-1f3fd · 1f3c4-1f3fd-200d-2640-fe0f · 1f3c4-1f3fd-200d-2642-fe0f · 1f3c4-1f3fe · 1f3c4-1f3fe-200d-2640-fe0f · 1f3c4-1f3fe-200d-2642-fe0f · 1f3c4-1f3ff · 1f3c4-1f3ff-200d-2640-fe0f · 1f3c4-1f3ff-200d-2642-fe0f · 1f3c4-200d-2640-fe0f · 1f3c4-200d-2642-fe0f · 1f3c5 · 1f3c6 · 1f3c7 · 1f3c7-1f3fb · 1f3c7-1f3fc · 1f3c7-1f3fd · 1f3c7-1f3fe · 1f3c7-1f3ff · 1f3c8 · 1f3c9 · 1f3ca · 1f3ca-1f3fb · 1f3ca-1f3fb-200d-2640-fe0f · 1f3ca-1f3fb-200d-2642-fe0f · 1f3ca-1f3fc · 1f3ca-1f3fc-200d-2640-fe0f · 1f3ca-1f3fc-200d-2642-fe0f · 1f3ca-1f3fd · 1f3ca-1f3fd-200d-2640-fe0f · 1f3ca-1f3fd-200d-2642-fe0f · 1f3ca-1f3fe · 1f3ca-1f3fe-200d-2640-fe0f · 1f3ca-1f3fe-200d-2642-fe0f · 1f3ca-1f3ff · 1f3ca-1f3ff-200d-2640-fe0f · 1f3ca-1f3ff-200d-2642-fe0f · 1f3ca-200d-2640-fe0f · 1f3ca-200d-2642-fe0f · 1f3cb · 1f3cb-1f3fb · 1f3cb-1f3fb-200d-2640-fe0f · 1f3cb-1f3fb-200d-2642-fe0f · 1f3cb-1f3fc · 1f3cb-1f3fc-200d-2640-fe0f · 1f3cb-1f3fc-200d-2642-fe0f · 1f3cb-1f3fd · 1f3cb-1f3fd-200d-2640-fe0f · 1f3cb-1f3fd-200d-2642-fe0f · 1f3cb-1f3fe · 1f3cb-1f3fe-200d-2640-fe0f · 1f3cb-1f3fe-200d-2642-fe0f · 1f3cb-1f3ff · 1f3cb-1f3ff-200d-2640-fe0f · 1f3cb-1f3ff-200d-2642-fe0f · 1f3cb-fe0f-200d-2640-fe0f · 1f3cb-fe0f-200d-2642-fe0f · 1f3cc · 1f3cc-1f3fb · 1f3cc-1f3fb-200d-2640-fe0f · 1f3cc-1f3fb-200d-2642-fe0f · 1f3cc-1f3fc · 1f3cc-1f3fc-200d-2640-fe0f · 1f3cc-1f3fc-200d-2642-fe0f · 1f3cc-1f3fd · 1f3cc-1f3fd-200d-2640-fe0f · 1f3cc-1f3fd-200d-2642-fe0f · 1f3cc-1f3fe · 1f3cc-1f3fe-200d-2640-fe0f · 1f3cc-1f3fe-200d-2642-fe0f · 1f3cc-1f3ff · 1f3cc-1f3ff-200d-2640-fe0f · 1f3cc-1f3ff-200d-2642-fe0f · 1f3cc-fe0f-200d-2640-fe0f · 1f3cc-fe0f-200d-2642-fe0f · 1f3cd · 1f3ce · 1f3cf · 1f3d0 · 1f3d1 · 1f3d2 · 1f3d3 · 1f3d4 · 1f3d5 · 1f3d6 · 1f3d7 · 1f3d8 · 1f3d9 · 1f3da · 1f3db · 1f3dc · 1f3dd · 1f3de · 1f3df · 1f3e0 · 1f3e1 · 1f3e2 · 1f3e3 · 1f3e4 · 1f3e5 · 1f3e6 · 1f3e7 · 1f3e8 · 1f3e9 · 1f3ea · 1f3eb · 1f3ec · 1f3ed · 1f3ee · 1f3ef · 1f3f0 · 1f3f3 · 1f3f3-fe0f-200d-1f308 · 1f3f3-fe0f-200d-26a7-fe0f · 1f3f4 · 1f3f4-200d-2620-fe0f · 1f3f4-e0067-e0062-e0065-e006e-e0067-e007f · 1f3f4-e0067-e0062-e0073-e0063-e0074-e007f · 1f3f4-e0067-e0062-e0077-e006c-e0073-e007f · 1f3f5 · 1f3f7 · 1f3f8 · 1f3f9 · 1f3fa · 1f3fb · 1f3fc · 1f3fd · 1f3fe · 1f3ff · 1f400 · 1f401 · 1f402 · 1f403 · 1f404 · 1f405 · 1f406 · 1f407 · 1f408 · 1f408-200d-2b1b · 1f409 · 1f40a · 1f40b · 1f40c · 1f40d · 1f40e · 1f40f · 1f410 · 1f411 · 1f412 · 1f413 · 1f414 · 1f415 · 1f415-200d-1f9ba · 1f416 · 1f417 · 1f418 · 1f419 · 1f41a · 1f41b · 1f41c · 1f41d · 1f41e · 1f41f · 1f420 · 1f421 · 1f422 · 1f423 · 1f424 · 1f425 · 1f426 · 1f426-200d-1f525 · 1f426-200d-2b1b · 1f427 · 1f428 · 1f429 · 1f42a · 1f42b · 1f42c · 1f42d · 1f42e · 1f42f · 1f430 · 1f431 · 1f432 · 1f433 · 1f434 · 1f435 · 1f436 · 1f437 · 1f438 · 1f439 · 1f43a · 1f43b · 1f43b-200d-2744-fe0f · 1f43c · 1f43d · 1f43e · 1f43f · 1f440 · 1f441 · 1f441-200d-1f5e8 · 1f442 · 1f442-1f3fb · 1f442-1f3fc · 1f442-1f3fd · 1f442-1f3fe · 1f442-1f3ff · 1f443 · 1f443-1f3fb · 1f443-1f3fc · 1f443-1f3fd · 1f443-1f3fe · 1f443-1f3ff · 1f444 · 1f445 · 1f446 · 1f446-1f3fb · 1f446-1f3fc · 1f446-1f3fd · 1f446-1f3fe · 1f446-1f3ff · 1f447 · 1f447-1f3fb · 1f447-1f3fc · 1f447-1f3fd · 1f447-1f3fe · 1f447-1f3ff · 1f448 · 1f448-1f3fb · 1f448-1f3fc · 1f448-1f3fd · 1f448-1f3fe · 1f448-1f3ff · 1f449 · 1f449-1f3fb · 1f449-1f3fc · 1f449-1f3fd · 1f449-1f3fe · 1f449-1f3ff · 1f44a · 1f44a-1f3fb · 1f44a-1f3fc · 1f44a-1f3fd · 1f44a-1f3fe · 1f44a-1f3ff · 1f44b · 1f44b-1f3fb · 1f44b-1f3fc · 1f44b-1f3fd · 1f44b-1f3fe · 1f44b-1f3ff · 1f44c · 1f44c-1f3fb · 1f44c-1f3fc · 1f44c-1f3fd · 1f44c-1f3fe · 1f44c-1f3ff · 1f44d · 1f44d-1f3fb · 1f44d-1f3fc · 1f44d-1f3fd · 1f44d-1f3fe · 1f44d-1f3ff · 1f44e · 1f44e-1f3fb · 1f44e-1f3fc · 1f44e-1f3fd · 1f44e-1f3fe · 1f44e-1f3ff · 1f44f · 1f44f-1f3fb · 1f44f-1f3fc · 1f44f-1f3fd · 1f44f-1f3fe · 1f44f-1f3ff · 1f450 · 1f450-1f3fb · 1f450-1f3fc · 1f450-1f3fd · 1f450-1f3fe · 1f450-1f3ff · 1f451 · 1f452 · 1f453 · 1f454 · 1f455 · 1f456 · 1f457 · 1f458 · 1f459 · 1f45a · 1f45b · 1f45c · 1f45d · 1f45e · 1f45f · 1f460 · 1f461 · 1f462 · 1f463 · 1f464 · 1f465 · 1f466 · 1f466-1f3fb · 1f466-1f3fc · 1f466-1f3fd · 1f466-1f3fe · 1f466-1f3ff · 1f467 · 1f467-1f3fb · 1f467-1f3fc · 1f467-1f3fd · 1f467-1f3fe · 1f467-1f3ff · 1f468 · 1f468-1f3fb · 1f468-1f3fb-200d-1f33e · 1f468-1f3fb-200d-1f373 · 1f468-1f3fb-200d-1f37c · 1f468-1f3fb-200d-1f384 · 1f468-1f3fb-200d-1f393 · 1f468-1f3fb-200d-1f3a4 · 1f468-1f3fb-200d-1f3a8 · 1f468-1f3fb-200d-1f3eb · 1f468-1f3fb-200d-1f3ed · 1f468-1f3fb-200d-1f430-200d-1f468-1f3fc · 1f468-1f3fb-200d-1f430-200d-1f468-1f3fd · 1f468-1f3fb-200d-1f430-200d-1f468-1f3fe · 1f468-1f3fb-200d-1f430-200d-1f468-1f3ff · 1f468-1f3fb-200d-1f4bb · 1f468-1f3fb-200d-1f4bc · 1f468-1f3fb-200d-1f527 · 1f468-1f3fb-200d-1f52c · 1f468-1f3fb-200d-1f680 · 1f468-1f3fb-200d-1f692 · 1f468-1f3fb-200d-1f91d-200d-1f468-1f3fc · 1f468-1f3fb-200d-1f91d-200d-1f468-1f3fd · 1f468-1f3fb-200d-1f91d-200d-1f468-1f3fe · 1f468-1f3fb-200d-1f91d-200d-1f468-1f3ff · 1f468-1f3fb-200d-1f9af · 1f468-1f3fb-200d-1f9af-200d-27a1-fe0f · 1f468-1f3fb-200d-1f9b0 · 1f468-1f3fb-200d-1f9b1 · 1f468-1f3fb-200d-1f9b2 · 1f468-1f3fb-200d-1f9b3 · 1f468-1f3fb-200d-1f9bc · 1f468-1f3fb-200d-1f9bc-200d-27a1-fe0f · 1f468-1f3fb-200d-1f9bd · 1f468-1f3fb-200d-1f9bd-200d-27a1-fe0f · 1f468-1f3fb-200d-1faef-200d-1f468-1f3fc · 1f468-1f3fb-200d-1faef-200d-1f468-1f3fd · 1f468-1f3fb-200d-1faef-200d-1f468-1f3fe · 1f468-1f3fb-200d-1faef-200d-1f468-1f3ff · 1f468-1f3fb-200d-2695-fe0f · 1f468-1f3fb-200d-2696-fe0f · 1f468-1f3fb-200d-2708-fe0f · 1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb · 1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc · 1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd · 1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe · 1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff · 1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f468-1f3fc · 1f468-1f3fc-200d-1f33e · 1f468-1f3fc-200d-1f373 · 1f468-1f3fc-200d-1f37c · 1f468-1f3fc-200d-1f384 · 1f468-1f3fc-200d-1f393 · 1f468-1f3fc-200d-1f3a4 · 1f468-1f3fc-200d-1f3a8 · 1f468-1f3fc-200d-1f3eb · 1f468-1f3fc-200d-1f3ed · 1f468-1f3fc-200d-1f430-200d-1f468-1f3fb · 1f468-1f3fc-200d-1f430-200d-1f468-1f3fd · 1f468-1f3fc-200d-1f430-200d-1f468-1f3fe · 1f468-1f3fc-200d-1f430-200d-1f468-1f3ff · 1f468-1f3fc-200d-1f4bb · 1f468-1f3fc-200d-1f4bc · 1f468-1f3fc-200d-1f527 · 1f468-1f3fc-200d-1f52c · 1f468-1f3fc-200d-1f680 · 1f468-1f3fc-200d-1f692 · 1f468-1f3fc-200d-1f91d-200d-1f468-1f3fb · 1f468-1f3fc-200d-1f91d-200d-1f468-1f3fd · 1f468-1f3fc-200d-1f91d-200d-1f468-1f3fe · 1f468-1f3fc-200d-1f91d-200d-1f468-1f3ff · 1f468-1f3fc-200d-1f9af · 1f468-1f3fc-200d-1f9af-200d-27a1-fe0f · 1f468-1f3fc-200d-1f9b0 · 1f468-1f3fc-200d-1f9b1 · 1f468-1f3fc-200d-1f9b2 · 1f468-1f3fc-200d-1f9b3 · 1f468-1f3fc-200d-1f9bc · 1f468-1f3fc-200d-1f9bc-200d-27a1-fe0f · 1f468-1f3fc-200d-1f9bd · 1f468-1f3fc-200d-1f9bd-200d-27a1-fe0f · 1f468-1f3fc-200d-1faef-200d-1f468-1f3fb · 1f468-1f3fc-200d-1faef-200d-1f468-1f3fd · 1f468-1f3fc-200d-1faef-200d-1f468-1f3fe · 1f468-1f3fc-200d-1faef-200d-1f468-1f3ff · 1f468-1f3fc-200d-2695-fe0f · 1f468-1f3fc-200d-2696-fe0f · 1f468-1f3fc-200d-2708-fe0f · 1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb · 1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc · 1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd · 1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe · 1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff · 1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f468-1f3fd · 1f468-1f3fd-200d-1f33e · 1f468-1f3fd-200d-1f373 · 1f468-1f3fd-200d-1f37c · 1f468-1f3fd-200d-1f384 · 1f468-1f3fd-200d-1f393 · 1f468-1f3fd-200d-1f3a4 · 1f468-1f3fd-200d-1f3a8 · 1f468-1f3fd-200d-1f3eb · 1f468-1f3fd-200d-1f3ed · 1f468-1f3fd-200d-1f430-200d-1f468-1f3fb · 1f468-1f3fd-200d-1f430-200d-1f468-1f3fc · 1f468-1f3fd-200d-1f430-200d-1f468-1f3fe · 1f468-1f3fd-200d-1f430-200d-1f468-1f3ff · 1f468-1f3fd-200d-1f4bb · 1f468-1f3fd-200d-1f4bc · 1f468-1f3fd-200d-1f527 · 1f468-1f3fd-200d-1f52c · 1f468-1f3fd-200d-1f680 · 1f468-1f3fd-200d-1f692 · 1f468-1f3fd-200d-1f91d-200d-1f468-1f3fb · 1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc · 1f468-1f3fd-200d-1f91d-200d-1f468-1f3fe · 1f468-1f3fd-200d-1f91d-200d-1f468-1f3ff · 1f468-1f3fd-200d-1f9af · 1f468-1f3fd-200d-1f9af-200d-27a1-fe0f · 1f468-1f3fd-200d-1f9b0 · 1f468-1f3fd-200d-1f9b1 · 1f468-1f3fd-200d-1f9b2 · 1f468-1f3fd-200d-1f9b3 · 1f468-1f3fd-200d-1f9bc · 1f468-1f3fd-200d-1f9bc-200d-27a1-fe0f · 1f468-1f3fd-200d-1f9bd · 1f468-1f3fd-200d-1f9bd-200d-27a1-fe0f · 1f468-1f3fd-200d-1faef-200d-1f468-1f3fb · 1f468-1f3fd-200d-1faef-200d-1f468-1f3fc · 1f468-1f3fd-200d-1faef-200d-1f468-1f3fe · 1f468-1f3fd-200d-1faef-200d-1f468-1f3ff · 1f468-1f3fd-200d-2695-fe0f · 1f468-1f3fd-200d-2696-fe0f · 1f468-1f3fd-200d-2708-fe0f · 1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb · 1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc · 1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd · 1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe · 1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff · 1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f468-1f3fe · 1f468-1f3fe-200d-1f33e · 1f468-1f3fe-200d-1f373 · 1f468-1f3fe-200d-1f37c · 1f468-1f3fe-200d-1f384 · 1f468-1f3fe-200d-1f393 · 1f468-1f3fe-200d-1f3a4 · 1f468-1f3fe-200d-1f3a8 · 1f468-1f3fe-200d-1f3eb · 1f468-1f3fe-200d-1f3ed · 1f468-1f3fe-200d-1f430-200d-1f468-1f3fb · 1f468-1f3fe-200d-1f430-200d-1f468-1f3fc · 1f468-1f3fe-200d-1f430-200d-1f468-1f3fd · 1f468-1f3fe-200d-1f430-200d-1f468-1f3ff · 1f468-1f3fe-200d-1f4bb · 1f468-1f3fe-200d-1f4bc · 1f468-1f3fe-200d-1f527 · 1f468-1f3fe-200d-1f52c · 1f468-1f3fe-200d-1f680 · 1f468-1f3fe-200d-1f692 · 1f468-1f3fe-200d-1f91d-200d-1f468-1f3fb · 1f468-1f3fe-200d-1f91d-200d-1f468-1f3fc · 1f468-1f3fe-200d-1f91d-200d-1f468-1f3fd · 1f468-1f3fe-200d-1f91d-200d-1f468-1f3ff · 1f468-1f3fe-200d-1f9af · 1f468-1f3fe-200d-1f9af-200d-27a1-fe0f · 1f468-1f3fe-200d-1f9b0 · 1f468-1f3fe-200d-1f9b1 · 1f468-1f3fe-200d-1f9b2 · 1f468-1f3fe-200d-1f9b3 · 1f468-1f3fe-200d-1f9bc · 1f468-1f3fe-200d-1f9bc-200d-27a1-fe0f · 1f468-1f3fe-200d-1f9bd · 1f468-1f3fe-200d-1f9bd-200d-27a1-fe0f · 1f468-1f3fe-200d-1faef-200d-1f468-1f3fb · 1f468-1f3fe-200d-1faef-200d-1f468-1f3fc · 1f468-1f3fe-200d-1faef-200d-1f468-1f3fd · 1f468-1f3fe-200d-1faef-200d-1f468-1f3ff · 1f468-1f3fe-200d-2695-fe0f · 1f468-1f3fe-200d-2696-fe0f · 1f468-1f3fe-200d-2708-fe0f · 1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb · 1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc · 1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd · 1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe · 1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff · 1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f468-1f3ff · 1f468-1f3ff-200d-1f33e · 1f468-1f3ff-200d-1f373 · 1f468-1f3ff-200d-1f37c · 1f468-1f3ff-200d-1f384 · 1f468-1f3ff-200d-1f393 · 1f468-1f3ff-200d-1f3a4 · 1f468-1f3ff-200d-1f3a8 · 1f468-1f3ff-200d-1f3eb · 1f468-1f3ff-200d-1f3ed · 1f468-1f3ff-200d-1f430-200d-1f468-1f3fb · 1f468-1f3ff-200d-1f430-200d-1f468-1f3fc · 1f468-1f3ff-200d-1f430-200d-1f468-1f3fd · 1f468-1f3ff-200d-1f430-200d-1f468-1f3fe · 1f468-1f3ff-200d-1f4bb · 1f468-1f3ff-200d-1f4bc · 1f468-1f3ff-200d-1f527 · 1f468-1f3ff-200d-1f52c · 1f468-1f3ff-200d-1f680 · 1f468-1f3ff-200d-1f692 · 1f468-1f3ff-200d-1f91d-200d-1f468-1f3fb · 1f468-1f3ff-200d-1f91d-200d-1f468-1f3fc · 1f468-1f3ff-200d-1f91d-200d-1f468-1f3fd · 1f468-1f3ff-200d-1f91d-200d-1f468-1f3fe · 1f468-1f3ff-200d-1f9af · 1f468-1f3ff-200d-1f9af-200d-27a1-fe0f · 1f468-1f3ff-200d-1f9b0 · 1f468-1f3ff-200d-1f9b1 · 1f468-1f3ff-200d-1f9b2 · 1f468-1f3ff-200d-1f9b3 · 1f468-1f3ff-200d-1f9bc · 1f468-1f3ff-200d-1f9bc-200d-27a1-fe0f · 1f468-1f3ff-200d-1f9bd · 1f468-1f3ff-200d-1f9bd-200d-27a1-fe0f · 1f468-1f3ff-200d-1faef-200d-1f468-1f3fb · 1f468-1f3ff-200d-1faef-200d-1f468-1f3fc · 1f468-1f3ff-200d-1faef-200d-1f468-1f3fd · 1f468-1f3ff-200d-1faef-200d-1f468-1f3fe · 1f468-1f3ff-200d-2695-fe0f · 1f468-1f3ff-200d-2696-fe0f · 1f468-1f3ff-200d-2708-fe0f · 1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb · 1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc · 1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd · 1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe · 1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff · 1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f468-200d-1f33e · 1f468-200d-1f373 · 1f468-200d-1f37c · 1f468-200d-1f384 · 1f468-200d-1f393 · 1f468-200d-1f3a4 · 1f468-200d-1f3a8 · 1f468-200d-1f3eb · 1f468-200d-1f3ed · 1f468-200d-1f466 · 1f468-200d-1f466-200d-1f466 · 1f468-200d-1f467 · 1f468-200d-1f467-200d-1f466 · 1f468-200d-1f467-200d-1f467 · 1f468-200d-1f468-200d-1f466 · 1f468-200d-1f468-200d-1f466-200d-1f466 · 1f468-200d-1f468-200d-1f467 · 1f468-200d-1f468-200d-1f467-200d-1f466 · 1f468-200d-1f468-200d-1f467-200d-1f467 · 1f468-200d-1f469-200d-1f466 · 1f468-200d-1f469-200d-1f466-200d-1f466 · 1f468-200d-1f469-200d-1f467 · 1f468-200d-1f469-200d-1f467-200d-1f466 · 1f468-200d-1f469-200d-1f467-200d-1f467 · 1f468-200d-1f4bb · 1f468-200d-1f4bc · 1f468-200d-1f527 · 1f468-200d-1f52c · 1f468-200d-1f680 · 1f468-200d-1f692 · 1f468-200d-1f9af · 1f468-200d-1f9af-200d-27a1-fe0f · 1f468-200d-1f9b0 · 1f468-200d-1f9b1 · 1f468-200d-1f9b2 · 1f468-200d-1f9b3 · 1f468-200d-1f9bc · 1f468-200d-1f9bc-200d-27a1-fe0f · 1f468-200d-1f9bd · 1f468-200d-1f9bd-200d-27a1-fe0f · 1f468-200d-2695-fe0f · 1f468-200d-2696-fe0f · 1f468-200d-2708-fe0f · 1f468-200d-2764-fe0f-200d-1f468 · 1f468-200d-2764-fe0f-200d-1f48b-200d-1f468 · 1f469 · 1f469-1f3fb · 1f469-1f3fb-200d-1f33e · 1f469-1f3fb-200d-1f373 · 1f469-1f3fb-200d-1f37c · 1f469-1f3fb-200d-1f384 · 1f469-1f3fb-200d-1f393 · 1f469-1f3fb-200d-1f3a4 · 1f469-1f3fb-200d-1f3a8 · 1f469-1f3fb-200d-1f3eb · 1f469-1f3fb-200d-1f3ed · 1f469-1f3fb-200d-1f430-200d-1f469-1f3fc · 1f469-1f3fb-200d-1f430-200d-1f469-1f3fd · 1f469-1f3fb-200d-1f430-200d-1f469-1f3fe · 1f469-1f3fb-200d-1f430-200d-1f469-1f3ff · 1f469-1f3fb-200d-1f4bb · 1f469-1f3fb-200d-1f4bc · 1f469-1f3fb-200d-1f527 · 1f469-1f3fb-200d-1f52c · 1f469-1f3fb-200d-1f680 · 1f469-1f3fb-200d-1f692 · 1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc · 1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd · 1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe · 1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff · 1f469-1f3fb-200d-1f91d-200d-1f469-1f3fc · 1f469-1f3fb-200d-1f91d-200d-1f469-1f3fd · 1f469-1f3fb-200d-1f91d-200d-1f469-1f3fe · 1f469-1f3fb-200d-1f91d-200d-1f469-1f3ff · 1f469-1f3fb-200d-1f9af · 1f469-1f3fb-200d-1f9af-200d-27a1-fe0f · 1f469-1f3fb-200d-1f9b0 · 1f469-1f3fb-200d-1f9b1 · 1f469-1f3fb-200d-1f9b2 · 1f469-1f3fb-200d-1f9b3 · 1f469-1f3fb-200d-1f9bc · 1f469-1f3fb-200d-1f9bc-200d-27a1-fe0f · 1f469-1f3fb-200d-1f9bd · 1f469-1f3fb-200d-1f9bd-200d-27a1-fe0f · 1f469-1f3fb-200d-1faef-200d-1f469-1f3fc · 1f469-1f3fb-200d-1faef-200d-1f469-1f3fd · 1f469-1f3fb-200d-1faef-200d-1f469-1f3fe · 1f469-1f3fb-200d-1faef-200d-1f469-1f3ff · 1f469-1f3fb-200d-2695-fe0f · 1f469-1f3fb-200d-2696-fe0f · 1f469-1f3fb-200d-2708-fe0f · 1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb · 1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc · 1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd · 1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe · 1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff · 1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fb · 1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fc · 1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fd · 1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fe · 1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3ff · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe · 1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff · 1f469-1f3fc · 1f469-1f3fc-200d-1f33e · 1f469-1f3fc-200d-1f373 · 1f469-1f3fc-200d-1f37c · 1f469-1f3fc-200d-1f384 · 1f469-1f3fc-200d-1f393 · 1f469-1f3fc-200d-1f3a4 · 1f469-1f3fc-200d-1f3a8 · 1f469-1f3fc-200d-1f3eb · 1f469-1f3fc-200d-1f3ed · 1f469-1f3fc-200d-1f430-200d-1f469-1f3fb · 1f469-1f3fc-200d-1f430-200d-1f469-1f3fd · 1f469-1f3fc-200d-1f430-200d-1f469-1f3fe · 1f469-1f3fc-200d-1f430-200d-1f469-1f3ff · 1f469-1f3fc-200d-1f4bb · 1f469-1f3fc-200d-1f4bc · 1f469-1f3fc-200d-1f527 · 1f469-1f3fc-200d-1f52c · 1f469-1f3fc-200d-1f680 · 1f469-1f3fc-200d-1f692 · 1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb · 1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd · 1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe · 1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff · 1f469-1f3fc-200d-1f91d-200d-1f469-1f3fb · 1f469-1f3fc-200d-1f91d-200d-1f469-1f3fd · 1f469-1f3fc-200d-1f91d-200d-1f469-1f3fe · 1f469-1f3fc-200d-1f91d-200d-1f469-1f3ff · 1f469-1f3fc-200d-1f9af · 1f469-1f3fc-200d-1f9af-200d-27a1-fe0f · 1f469-1f3fc-200d-1f9b0 · 1f469-1f3fc-200d-1f9b1 · 1f469-1f3fc-200d-1f9b2 · 1f469-1f3fc-200d-1f9b3 · 1f469-1f3fc-200d-1f9bc · 1f469-1f3fc-200d-1f9bc-200d-27a1-fe0f · 1f469-1f3fc-200d-1f9bd · 1f469-1f3fc-200d-1f9bd-200d-27a1-fe0f · 1f469-1f3fc-200d-1faef-200d-1f469-1f3fb · 1f469-1f3fc-200d-1faef-200d-1f469-1f3fd · 1f469-1f3fc-200d-1faef-200d-1f469-1f3fe · 1f469-1f3fc-200d-1faef-200d-1f469-1f3ff · 1f469-1f3fc-200d-2695-fe0f · 1f469-1f3fc-200d-2696-fe0f · 1f469-1f3fc-200d-2708-fe0f · 1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb · 1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc · 1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd · 1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe · 1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff · 1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fb · 1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fc · 1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fd · 1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fe · 1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3ff · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe · 1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff · 1f469-1f3fd · 1f469-1f3fd-200d-1f33e · 1f469-1f3fd-200d-1f373 · 1f469-1f3fd-200d-1f37c · 1f469-1f3fd-200d-1f384 · 1f469-1f3fd-200d-1f393 · 1f469-1f3fd-200d-1f3a4 · 1f469-1f3fd-200d-1f3a8 · 1f469-1f3fd-200d-1f3eb · 1f469-1f3fd-200d-1f3ed · 1f469-1f3fd-200d-1f430-200d-1f469-1f3fb · 1f469-1f3fd-200d-1f430-200d-1f469-1f3fc · 1f469-1f3fd-200d-1f430-200d-1f469-1f3fe · 1f469-1f3fd-200d-1f430-200d-1f469-1f3ff · 1f469-1f3fd-200d-1f4bb · 1f469-1f3fd-200d-1f4bc · 1f469-1f3fd-200d-1f527 · 1f469-1f3fd-200d-1f52c · 1f469-1f3fd-200d-1f680 · 1f469-1f3fd-200d-1f692 · 1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb · 1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc · 1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe · 1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff · 1f469-1f3fd-200d-1f91d-200d-1f469-1f3fb · 1f469-1f3fd-200d-1f91d-200d-1f469-1f3fc · 1f469-1f3fd-200d-1f91d-200d-1f469-1f3fe · 1f469-1f3fd-200d-1f91d-200d-1f469-1f3ff · 1f469-1f3fd-200d-1f9af · 1f469-1f3fd-200d-1f9af-200d-27a1-fe0f · 1f469-1f3fd-200d-1f9b0 · 1f469-1f3fd-200d-1f9b1 · 1f469-1f3fd-200d-1f9b2 · 1f469-1f3fd-200d-1f9b3 · 1f469-1f3fd-200d-1f9bc · 1f469-1f3fd-200d-1f9bc-200d-27a1-fe0f · 1f469-1f3fd-200d-1f9bd · 1f469-1f3fd-200d-1f9bd-200d-27a1-fe0f · 1f469-1f3fd-200d-1faef-200d-1f469-1f3fb · 1f469-1f3fd-200d-1faef-200d-1f469-1f3fc · 1f469-1f3fd-200d-1faef-200d-1f469-1f3fe · 1f469-1f3fd-200d-1faef-200d-1f469-1f3ff · 1f469-1f3fd-200d-2695-fe0f · 1f469-1f3fd-200d-2696-fe0f · 1f469-1f3fd-200d-2708-fe0f · 1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb · 1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc · 1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd · 1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe · 1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff · 1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fb · 1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fc · 1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fd · 1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fe · 1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3ff · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe · 1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff · 1f469-1f3fe · 1f469-1f3fe-200d-1f33e · 1f469-1f3fe-200d-1f373 · 1f469-1f3fe-200d-1f37c · 1f469-1f3fe-200d-1f384 · 1f469-1f3fe-200d-1f393 · 1f469-1f3fe-200d-1f3a4 · 1f469-1f3fe-200d-1f3a8 · 1f469-1f3fe-200d-1f3eb · 1f469-1f3fe-200d-1f3ed · 1f469-1f3fe-200d-1f430-200d-1f469-1f3fb · 1f469-1f3fe-200d-1f430-200d-1f469-1f3fc · 1f469-1f3fe-200d-1f430-200d-1f469-1f3fd · 1f469-1f3fe-200d-1f430-200d-1f469-1f3ff · 1f469-1f3fe-200d-1f4bb · 1f469-1f3fe-200d-1f4bc · 1f469-1f3fe-200d-1f527 · 1f469-1f3fe-200d-1f52c · 1f469-1f3fe-200d-1f680 · 1f469-1f3fe-200d-1f692 · 1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb · 1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc · 1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd · 1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff · 1f469-1f3fe-200d-1f91d-200d-1f469-1f3fb · 1f469-1f3fe-200d-1f91d-200d-1f469-1f3fc · 1f469-1f3fe-200d-1f91d-200d-1f469-1f3fd · 1f469-1f3fe-200d-1f91d-200d-1f469-1f3ff · 1f469-1f3fe-200d-1f9af · 1f469-1f3fe-200d-1f9af-200d-27a1-fe0f · 1f469-1f3fe-200d-1f9b0 · 1f469-1f3fe-200d-1f9b1 · 1f469-1f3fe-200d-1f9b2 · 1f469-1f3fe-200d-1f9b3 · 1f469-1f3fe-200d-1f9bc · 1f469-1f3fe-200d-1f9bc-200d-27a1-fe0f · 1f469-1f3fe-200d-1f9bd · 1f469-1f3fe-200d-1f9bd-200d-27a1-fe0f · 1f469-1f3fe-200d-1faef-200d-1f469-1f3fb · 1f469-1f3fe-200d-1faef-200d-1f469-1f3fc · 1f469-1f3fe-200d-1faef-200d-1f469-1f3fd · 1f469-1f3fe-200d-1faef-200d-1f469-1f3ff · 1f469-1f3fe-200d-2695-fe0f · 1f469-1f3fe-200d-2696-fe0f · 1f469-1f3fe-200d-2708-fe0f · 1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb · 1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc · 1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd · 1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe · 1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff · 1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fb · 1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fc · 1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fd · 1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fe · 1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3ff · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe · 1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff · 1f469-1f3ff · 1f469-1f3ff-200d-1f33e · 1f469-1f3ff-200d-1f373 · 1f469-1f3ff-200d-1f37c · 1f469-1f3ff-200d-1f384 · 1f469-1f3ff-200d-1f393 · 1f469-1f3ff-200d-1f3a4 · 1f469-1f3ff-200d-1f3a8 · 1f469-1f3ff-200d-1f3eb · 1f469-1f3ff-200d-1f3ed · 1f469-1f3ff-200d-1f430-200d-1f469-1f3fb · 1f469-1f3ff-200d-1f430-200d-1f469-1f3fc · 1f469-1f3ff-200d-1f430-200d-1f469-1f3fd · 1f469-1f3ff-200d-1f430-200d-1f469-1f3fe · 1f469-1f3ff-200d-1f4bb · 1f469-1f3ff-200d-1f4bc · 1f469-1f3ff-200d-1f527 · 1f469-1f3ff-200d-1f52c · 1f469-1f3ff-200d-1f680 · 1f469-1f3ff-200d-1f692 · 1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb · 1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc · 1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd · 1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe · 1f469-1f3ff-200d-1f91d-200d-1f469-1f3fb · 1f469-1f3ff-200d-1f91d-200d-1f469-1f3fc · 1f469-1f3ff-200d-1f91d-200d-1f469-1f3fd · 1f469-1f3ff-200d-1f91d-200d-1f469-1f3fe · 1f469-1f3ff-200d-1f9af · 1f469-1f3ff-200d-1f9af-200d-27a1-fe0f · 1f469-1f3ff-200d-1f9b0 · 1f469-1f3ff-200d-1f9b1 · 1f469-1f3ff-200d-1f9b2 · 1f469-1f3ff-200d-1f9b3 · 1f469-1f3ff-200d-1f9bc · 1f469-1f3ff-200d-1f9bc-200d-27a1-fe0f · 1f469-1f3ff-200d-1f9bd · 1f469-1f3ff-200d-1f9bd-200d-27a1-fe0f · 1f469-1f3ff-200d-1faef-200d-1f469-1f3fb · 1f469-1f3ff-200d-1faef-200d-1f469-1f3fc · 1f469-1f3ff-200d-1faef-200d-1f469-1f3fd · 1f469-1f3ff-200d-1faef-200d-1f469-1f3fe · 1f469-1f3ff-200d-2695-fe0f · 1f469-1f3ff-200d-2696-fe0f · 1f469-1f3ff-200d-2708-fe0f · 1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb · 1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc · 1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd · 1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe · 1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff · 1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fb · 1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fc · 1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fd · 1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fe · 1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3ff · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe · 1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff · 1f469-200d-1f33e · 1f469-200d-1f373 · 1f469-200d-1f37c · 1f469-200d-1f384 · 1f469-200d-1f393 · 1f469-200d-1f3a4 · 1f469-200d-1f3a8 · 1f469-200d-1f3eb · 1f469-200d-1f3ed · 1f469-200d-1f466 · 1f469-200d-1f466-200d-1f466 · 1f469-200d-1f467 · 1f469-200d-1f467-200d-1f466 · 1f469-200d-1f467-200d-1f467 · 1f469-200d-1f469-200d-1f466 · 1f469-200d-1f469-200d-1f466-200d-1f466 · 1f469-200d-1f469-200d-1f467 · 1f469-200d-1f469-200d-1f467-200d-1f466 · 1f469-200d-1f469-200d-1f467-200d-1f467 · 1f469-200d-1f4bb · 1f469-200d-1f4bc · 1f469-200d-1f527 · 1f469-200d-1f52c · 1f469-200d-1f680 · 1f469-200d-1f692 · 1f469-200d-1f9af · 1f469-200d-1f9af-200d-27a1-fe0f · 1f469-200d-1f9b0 · 1f469-200d-1f9b1 · 1f469-200d-1f9b2 · 1f469-200d-1f9b3 · 1f469-200d-1f9bc · 1f469-200d-1f9bc-200d-27a1-fe0f · 1f469-200d-1f9bd · 1f469-200d-1f9bd-200d-27a1-fe0f · 1f469-200d-2695-fe0f · 1f469-200d-2696-fe0f · 1f469-200d-2708-fe0f · 1f469-200d-2764-fe0f-200d-1f468 · 1f469-200d-2764-fe0f-200d-1f469 · 1f469-200d-2764-fe0f-200d-1f48b-200d-1f468 · 1f469-200d-2764-fe0f-200d-1f48b-200d-1f469 · 1f46a · 1f46b · 1f46b-1f3fb · 1f46b-1f3fc · 1f46b-1f3fd · 1f46b-1f3fe · 1f46b-1f3ff · 1f46c · 1f46c-1f3fb · 1f46c-1f3fc · 1f46c-1f3fd · 1f46c-1f3fe · 1f46c-1f3ff · 1f46d · 1f46d-1f3fb · 1f46d-1f3fc · 1f46d-1f3fd · 1f46d-1f3fe · 1f46d-1f3ff · 1f46e · 1f46e-1f3fb · 1f46e-1f3fb-200d-2640-fe0f · 1f46e-1f3fb-200d-2642-fe0f · 1f46e-1f3fc · 1f46e-1f3fc-200d-2640-fe0f · 1f46e-1f3fc-200d-2642-fe0f · 1f46e-1f3fd · 1f46e-1f3fd-200d-2640-fe0f · 1f46e-1f3fd-200d-2642-fe0f · 1f46e-1f3fe · 1f46e-1f3fe-200d-2640-fe0f · 1f46e-1f3fe-200d-2642-fe0f · 1f46e-1f3ff · 1f46e-1f3ff-200d-2640-fe0f · 1f46e-1f3ff-200d-2642-fe0f · 1f46e-200d-2640-fe0f · 1f46e-200d-2642-fe0f · 1f46f · 1f46f-1f3fb · 1f46f-1f3fb-200d-2640-fe0f · 1f46f-1f3fb-200d-2642-fe0f · 1f46f-1f3fc · 1f46f-1f3fc-200d-2640-fe0f · 1f46f-1f3fc-200d-2642-fe0f · 1f46f-1f3fd · 1f46f-1f3fd-200d-2640-fe0f · 1f46f-1f3fd-200d-2642-fe0f · 1f46f-1f3fe · 1f46f-1f3fe-200d-2640-fe0f · 1f46f-1f3fe-200d-2642-fe0f · 1f46f-1f3ff · 1f46f-1f3ff-200d-2640-fe0f · 1f46f-1f3ff-200d-2642-fe0f · 1f46f-200d-2640-fe0f · 1f46f-200d-2642-fe0f · 1f470 · 1f470-1f3fb · 1f470-1f3fb-200d-2640-fe0f · 1f470-1f3fb-200d-2642-fe0f · 1f470-1f3fc · 1f470-1f3fc-200d-2640-fe0f · 1f470-1f3fc-200d-2642-fe0f · 1f470-1f3fd · 1f470-1f3fd-200d-2640-fe0f · 1f470-1f3fd-200d-2642-fe0f · 1f470-1f3fe · 1f470-1f3fe-200d-2640-fe0f · 1f470-1f3fe-200d-2642-fe0f · 1f470-1f3ff · 1f470-1f3ff-200d-2640-fe0f · 1f470-1f3ff-200d-2642-fe0f · 1f470-200d-2640-fe0f · 1f470-200d-2642-fe0f · 1f471 · 1f471-1f3fb · 1f471-1f3fb-200d-2640-fe0f · 1f471-1f3fb-200d-2642-fe0f · 1f471-1f3fc · 1f471-1f3fc-200d-2640-fe0f · 1f471-1f3fc-200d-2642-fe0f · 1f471-1f3fd · 1f471-1f3fd-200d-2640-fe0f · 1f471-1f3fd-200d-2642-fe0f · 1f471-1f3fe · 1f471-1f3fe-200d-2640-fe0f · 1f471-1f3fe-200d-2642-fe0f · 1f471-1f3ff · 1f471-1f3ff-200d-2640-fe0f · 1f471-1f3ff-200d-2642-fe0f · 1f471-200d-2640-fe0f · 1f471-200d-2642-fe0f · 1f472 · 1f472-1f3fb · 1f472-1f3fc · 1f472-1f3fd · 1f472-1f3fe · 1f472-1f3ff · 1f473 · 1f473-1f3fb · 1f473-1f3fb-200d-2640-fe0f · 1f473-1f3fb-200d-2642-fe0f · 1f473-1f3fc · 1f473-1f3fc-200d-2640-fe0f · 1f473-1f3fc-200d-2642-fe0f · 1f473-1f3fd · 1f473-1f3fd-200d-2640-fe0f · 1f473-1f3fd-200d-2642-fe0f · 1f473-1f3fe · 1f473-1f3fe-200d-2640-fe0f · 1f473-1f3fe-200d-2642-fe0f · 1f473-1f3ff · 1f473-1f3ff-200d-2640-fe0f · 1f473-1f3ff-200d-2642-fe0f · 1f473-200d-2640-fe0f · 1f473-200d-2642-fe0f · 1f474 · 1f474-1f3fb · 1f474-1f3fc · 1f474-1f3fd · 1f474-1f3fe · 1f474-1f3ff · 1f475 · 1f475-1f3fb · 1f475-1f3fc · 1f475-1f3fd · 1f475-1f3fe · 1f475-1f3ff · 1f476 · 1f476-1f3fb · 1f476-1f3fc · 1f476-1f3fd · 1f476-1f3fe · 1f476-1f3ff · 1f477 · 1f477-1f3fb · 1f477-1f3fb-200d-2640-fe0f · 1f477-1f3fb-200d-2642-fe0f · 1f477-1f3fc · 1f477-1f3fc-200d-2640-fe0f · 1f477-1f3fc-200d-2642-fe0f · 1f477-1f3fd · 1f477-1f3fd-200d-2640-fe0f · 1f477-1f3fd-200d-2642-fe0f · 1f477-1f3fe · 1f477-1f3fe-200d-2640-fe0f · 1f477-1f3fe-200d-2642-fe0f · 1f477-1f3ff · 1f477-1f3ff-200d-2640-fe0f · 1f477-1f3ff-200d-2642-fe0f · 1f477-200d-2640-fe0f · 1f477-200d-2642-fe0f · 1f478 · 1f478-1f3fb · 1f478-1f3fc · 1f478-1f3fd · 1f478-1f3fe · 1f478-1f3ff · 1f479 · 1f47a · 1f47b · 1f47c · 1f47c-1f3fb · 1f47c-1f3fc · 1f47c-1f3fd · 1f47c-1f3fe · 1f47c-1f3ff · 1f47d · 1f47e · 1f47f · 1f480 · 1f481 · 1f481-1f3fb · 1f481-1f3fb-200d-2640-fe0f · 1f481-1f3fb-200d-2642-fe0f · 1f481-1f3fc · 1f481-1f3fc-200d-2640-fe0f · 1f481-1f3fc-200d-2642-fe0f · 1f481-1f3fd · 1f481-1f3fd-200d-2640-fe0f · 1f481-1f3fd-200d-2642-fe0f · 1f481-1f3fe · 1f481-1f3fe-200d-2640-fe0f · 1f481-1f3fe-200d-2642-fe0f · 1f481-1f3ff · 1f481-1f3ff-200d-2640-fe0f · 1f481-1f3ff-200d-2642-fe0f · 1f481-200d-2640-fe0f · 1f481-200d-2642-fe0f · 1f482 · 1f482-1f3fb · 1f482-1f3fb-200d-2640-fe0f · 1f482-1f3fb-200d-2642-fe0f · 1f482-1f3fc · 1f482-1f3fc-200d-2640-fe0f · 1f482-1f3fc-200d-2642-fe0f · 1f482-1f3fd · 1f482-1f3fd-200d-2640-fe0f · 1f482-1f3fd-200d-2642-fe0f · 1f482-1f3fe · 1f482-1f3fe-200d-2640-fe0f · 1f482-1f3fe-200d-2642-fe0f · 1f482-1f3ff · 1f482-1f3ff-200d-2640-fe0f · 1f482-1f3ff-200d-2642-fe0f · 1f482-200d-2640-fe0f · 1f482-200d-2642-fe0f · 1f483 · 1f483-1f3fb · 1f483-1f3fc · 1f483-1f3fd · 1f483-1f3fe · 1f483-1f3ff · 1f484 · 1f485 · 1f485-1f3fb · 1f485-1f3fc · 1f485-1f3fd · 1f485-1f3fe · 1f485-1f3ff · 1f486 · 1f486-1f3fb · 1f486-1f3fb-200d-2640-fe0f · 1f486-1f3fb-200d-2642-fe0f · 1f486-1f3fc · 1f486-1f3fc-200d-2640-fe0f · 1f486-1f3fc-200d-2642-fe0f · 1f486-1f3fd · 1f486-1f3fd-200d-2640-fe0f · 1f486-1f3fd-200d-2642-fe0f · 1f486-1f3fe · 1f486-1f3fe-200d-2640-fe0f · 1f486-1f3fe-200d-2642-fe0f · 1f486-1f3ff · 1f486-1f3ff-200d-2640-fe0f · 1f486-1f3ff-200d-2642-fe0f · 1f486-200d-2640-fe0f · 1f486-200d-2642-fe0f · 1f487 · 1f487-1f3fb · 1f487-1f3fb-200d-2640-fe0f · 1f487-1f3fb-200d-2642-fe0f · 1f487-1f3fc · 1f487-1f3fc-200d-2640-fe0f · 1f487-1f3fc-200d-2642-fe0f · 1f487-1f3fd · 1f487-1f3fd-200d-2640-fe0f · 1f487-1f3fd-200d-2642-fe0f · 1f487-1f3fe · 1f487-1f3fe-200d-2640-fe0f · 1f487-1f3fe-200d-2642-fe0f · 1f487-1f3ff · 1f487-1f3ff-200d-2640-fe0f · 1f487-1f3ff-200d-2642-fe0f · 1f487-200d-2640-fe0f · 1f487-200d-2642-fe0f · 1f488 · 1f489 · 1f48a · 1f48b · 1f48c · 1f48d · 1f48e · 1f48f · 1f48f-1f3fb · 1f48f-1f3fc · 1f48f-1f3fd · 1f48f-1f3fe · 1f48f-1f3ff · 1f490 · 1f491 · 1f491-1f3fb · 1f491-1f3fc · 1f491-1f3fd · 1f491-1f3fe · 1f491-1f3ff · 1f492 · 1f493 · 1f494 · 1f495 · 1f496 · 1f497 · 1f498 · 1f499 · 1f49a · 1f49b · 1f49c · 1f49d · 1f49e · 1f49f · 1f4a0 · 1f4a1 · 1f4a2 · 1f4a3 · 1f4a4 · 1f4a5 · 1f4a6 · 1f4a7 · 1f4a8 · 1f4a9 · 1f4aa · 1f4aa-1f3fb · 1f4aa-1f3fc · 1f4aa-1f3fd · 1f4aa-1f3fe · 1f4aa-1f3ff · 1f4ab · 1f4ac · 1f4ad · 1f4ae · 1f4af · 1f4b0 · 1f4b1 · 1f4b2 · 1f4b3 · 1f4b4 · 1f4b5 · 1f4b6 · 1f4b7 · 1f4b8 · 1f4b9 · 1f4ba · 1f4bb · 1f4bc · 1f4bd · 1f4be · 1f4bf · 1f4c0 · 1f4c1 · 1f4c2 · 1f4c3 · 1f4c4 · 1f4c5 · 1f4c6 · 1f4c7 · 1f4c8 · 1f4c9 · 1f4ca · 1f4cb · 1f4cc · 1f4cd · 1f4ce · 1f4cf · 1f4d0 · 1f4d1 · 1f4d2 · 1f4d3 · 1f4d4 · 1f4d5 · 1f4d6 · 1f4d7 · 1f4d8 · 1f4d9 · 1f4da · 1f4db · 1f4dc · 1f4dd · 1f4de · 1f4df · 1f4e0 · 1f4e1 · 1f4e2 · 1f4e3 · 1f4e4 · 1f4e5 · 1f4e6 · 1f4e7 · 1f4e8 · 1f4e9 · 1f4ea · 1f4eb · 1f4ec · 1f4ed · 1f4ee · 1f4ef · 1f4f0 · 1f4f1 · 1f4f2 · 1f4f3 · 1f4f4 · 1f4f5 · 1f4f6 · 1f4f7 · 1f4f8 · 1f4f9 · 1f4fa · 1f4fb · 1f4fc · 1f4fd · 1f4ff · 1f500 · 1f501 · 1f502 · 1f503 · 1f504 · 1f505 · 1f506 · 1f507 · 1f508 · 1f509 · 1f50a · 1f50b · 1f50c · 1f50d · 1f50e · 1f50f · 1f510 · 1f511 · 1f512 · 1f513 · 1f514 · 1f515 · 1f516 · 1f517 · 1f518 · 1f519 · 1f51a · 1f51b · 1f51c · 1f51d · 1f51e · 1f51f · 1f520 · 1f521 · 1f522 · 1f523 · 1f524 · 1f525 · 1f526 · 1f527 · 1f528 · 1f529 · 1f52a · 1f52b · 1f52c · 1f52d · 1f52e · 1f52f · 1f530 · 1f531 · 1f532 · 1f533 · 1f534 · 1f535 · 1f536 · 1f537 · 1f538 · 1f539 · 1f53a · 1f53b · 1f53c · 1f53d · 1f549 · 1f54a · 1f54b · 1f54c · 1f54d · 1f54e · 1f550 · 1f551 · 1f552 · 1f553 · 1f554 · 1f555 · 1f556 · 1f557 · 1f558 · 1f559 · 1f55a · 1f55b · 1f55c · 1f55d · 1f55e · 1f55f · 1f560 · 1f561 · 1f562 · 1f563 · 1f564 · 1f565 · 1f566 · 1f567 · 1f56f · 1f570 · 1f573 · 1f574 · 1f574-1f3fb · 1f574-1f3fb-200d-2640-fe0f · 1f574-1f3fb-200d-2642-fe0f · 1f574-1f3fc · 1f574-1f3fc-200d-2640-fe0f · 1f574-1f3fc-200d-2642-fe0f · 1f574-1f3fd · 1f574-1f3fd-200d-2640-fe0f · 1f574-1f3fd-200d-2642-fe0f · 1f574-1f3fe · 1f574-1f3fe-200d-2640-fe0f · 1f574-1f3fe-200d-2642-fe0f · 1f574-1f3ff · 1f574-1f3ff-200d-2640-fe0f · 1f574-1f3ff-200d-2642-fe0f · 1f574-fe0f-200d-2640-fe0f · 1f574-fe0f-200d-2642-fe0f · 1f575 · 1f575-1f3fb · 1f575-1f3fb-200d-2640-fe0f · 1f575-1f3fb-200d-2642-fe0f · 1f575-1f3fc · 1f575-1f3fc-200d-2640-fe0f · 1f575-1f3fc-200d-2642-fe0f · 1f575-1f3fd · 1f575-1f3fd-200d-2640-fe0f · 1f575-1f3fd-200d-2642-fe0f · 1f575-1f3fe · 1f575-1f3fe-200d-2640-fe0f · 1f575-1f3fe-200d-2642-fe0f · 1f575-1f3ff · 1f575-1f3ff-200d-2640-fe0f · 1f575-1f3ff-200d-2642-fe0f · 1f575-fe0f-200d-2640-fe0f · 1f575-fe0f-200d-2642-fe0f · 1f576 · 1f577 · 1f578 · 1f579 · 1f57a · 1f57a-1f3fb · 1f57a-1f3fc · 1f57a-1f3fd · 1f57a-1f3fe · 1f57a-1f3ff · 1f587 · 1f58a · 1f58b · 1f58c · 1f58d · 1f590 · 1f590-1f3fb · 1f590-1f3fc · 1f590-1f3fd · 1f590-1f3fe · 1f590-1f3ff · 1f595 · 1f595-1f3fb · 1f595-1f3fc · 1f595-1f3fd · 1f595-1f3fe · 1f595-1f3ff · 1f596 · 1f596-1f3fb · 1f596-1f3fc · 1f596-1f3fd · 1f596-1f3fe · 1f596-1f3ff · 1f5a4 · 1f5a5 · 1f5a8 · 1f5b1 · 1f5b2 · 1f5bc · 1f5c2 · 1f5c3 · 1f5c4 · 1f5d1 · 1f5d2 · 1f5d3 · 1f5dc · 1f5dd · 1f5de · 1f5e1 · 1f5e3 · 1f5e8 · 1f5ef · 1f5f3 · 1f5fa · 1f5fb · 1f5fc · 1f5fd · 1f5fe · 1f5ff · 1f600 · 1f601 · 1f602 · 1f603 · 1f604 · 1f605 · 1f606 · 1f607 · 1f608 · 1f609 · 1f60a · 1f60b · 1f60c · 1f60d · 1f60e · 1f60f · 1f610 · 1f611 · 1f612 · 1f613 · 1f614 · 1f615 · 1f616 · 1f617 · 1f618 · 1f619 · 1f61a · 1f61b · 1f61c · 1f61d · 1f61e · 1f61f · 1f620 · 1f621 · 1f622 · 1f623 · 1f624 · 1f625 · 1f626 · 1f627 · 1f628 · 1f629 · 1f62a · 1f62b · 1f62c · 1f62d · 1f62e · 1f62e-200d-1f4a8 · 1f62f · 1f630 · 1f631 · 1f632 · 1f633 · 1f634 · 1f635 · 1f635-200d-1f4ab · 1f636 · 1f636-200d-1f32b-fe0f · 1f637 · 1f638 · 1f639 · 1f63a · 1f63b · 1f63c · 1f63d · 1f63e · 1f63f · 1f640 · 1f641 · 1f642 · 1f642-200d-2194-fe0f · 1f642-200d-2195-fe0f · 1f643 · 1f644 · 1f645 · 1f645-1f3fb · 1f645-1f3fb-200d-2640-fe0f · 1f645-1f3fb-200d-2642-fe0f · 1f645-1f3fc · 1f645-1f3fc-200d-2640-fe0f · 1f645-1f3fc-200d-2642-fe0f · 1f645-1f3fd · 1f645-1f3fd-200d-2640-fe0f · 1f645-1f3fd-200d-2642-fe0f · 1f645-1f3fe · 1f645-1f3fe-200d-2640-fe0f · 1f645-1f3fe-200d-2642-fe0f · 1f645-1f3ff · 1f645-1f3ff-200d-2640-fe0f · 1f645-1f3ff-200d-2642-fe0f · 1f645-200d-2640-fe0f · 1f645-200d-2642-fe0f · 1f646 · 1f646-1f3fb · 1f646-1f3fb-200d-2640-fe0f · 1f646-1f3fb-200d-2642-fe0f · 1f646-1f3fc · 1f646-1f3fc-200d-2640-fe0f · 1f646-1f3fc-200d-2642-fe0f · 1f646-1f3fd · 1f646-1f3fd-200d-2640-fe0f · 1f646-1f3fd-200d-2642-fe0f · 1f646-1f3fe · 1f646-1f3fe-200d-2640-fe0f · 1f646-1f3fe-200d-2642-fe0f · 1f646-1f3ff · 1f646-1f3ff-200d-2640-fe0f · 1f646-1f3ff-200d-2642-fe0f · 1f646-200d-2640-fe0f · 1f646-200d-2642-fe0f · 1f647 · 1f647-1f3fb · 1f647-1f3fb-200d-2640-fe0f · 1f647-1f3fb-200d-2642-fe0f · 1f647-1f3fc · 1f647-1f3fc-200d-2640-fe0f · 1f647-1f3fc-200d-2642-fe0f · 1f647-1f3fd · 1f647-1f3fd-200d-2640-fe0f · 1f647-1f3fd-200d-2642-fe0f · 1f647-1f3fe · 1f647-1f3fe-200d-2640-fe0f · 1f647-1f3fe-200d-2642-fe0f · 1f647-1f3ff · 1f647-1f3ff-200d-2640-fe0f · 1f647-1f3ff-200d-2642-fe0f · 1f647-200d-2640-fe0f · 1f647-200d-2642-fe0f · 1f648 · 1f649 · 1f64a · 1f64b · 1f64b-1f3fb · 1f64b-1f3fb-200d-2640-fe0f · 1f64b-1f3fb-200d-2642-fe0f · 1f64b-1f3fc · 1f64b-1f3fc-200d-2640-fe0f · 1f64b-1f3fc-200d-2642-fe0f · 1f64b-1f3fd · 1f64b-1f3fd-200d-2640-fe0f · 1f64b-1f3fd-200d-2642-fe0f · 1f64b-1f3fe · 1f64b-1f3fe-200d-2640-fe0f · 1f64b-1f3fe-200d-2642-fe0f · 1f64b-1f3ff · 1f64b-1f3ff-200d-2640-fe0f · 1f64b-1f3ff-200d-2642-fe0f · 1f64b-200d-2640-fe0f · 1f64b-200d-2642-fe0f · 1f64c · 1f64c-1f3fb · 1f64c-1f3fc · 1f64c-1f3fd · 1f64c-1f3fe · 1f64c-1f3ff · 1f64d · 1f64d-1f3fb · 1f64d-1f3fb-200d-2640-fe0f · 1f64d-1f3fb-200d-2642-fe0f · 1f64d-1f3fc · 1f64d-1f3fc-200d-2640-fe0f · 1f64d-1f3fc-200d-2642-fe0f · 1f64d-1f3fd · 1f64d-1f3fd-200d-2640-fe0f · 1f64d-1f3fd-200d-2642-fe0f · 1f64d-1f3fe · 1f64d-1f3fe-200d-2640-fe0f · 1f64d-1f3fe-200d-2642-fe0f · 1f64d-1f3ff · 1f64d-1f3ff-200d-2640-fe0f · 1f64d-1f3ff-200d-2642-fe0f · 1f64d-200d-2640-fe0f · 1f64d-200d-2642-fe0f · 1f64e · 1f64e-1f3fb · 1f64e-1f3fb-200d-2640-fe0f · 1f64e-1f3fb-200d-2642-fe0f · 1f64e-1f3fc · 1f64e-1f3fc-200d-2640-fe0f · 1f64e-1f3fc-200d-2642-fe0f · 1f64e-1f3fd · 1f64e-1f3fd-200d-2640-fe0f · 1f64e-1f3fd-200d-2642-fe0f · 1f64e-1f3fe · 1f64e-1f3fe-200d-2640-fe0f · 1f64e-1f3fe-200d-2642-fe0f · 1f64e-1f3ff · 1f64e-1f3ff-200d-2640-fe0f · 1f64e-1f3ff-200d-2642-fe0f · 1f64e-200d-2640-fe0f · 1f64e-200d-2642-fe0f · 1f64f · 1f64f-1f3fb · 1f64f-1f3fc · 1f64f-1f3fd · 1f64f-1f3fe · 1f64f-1f3ff · 1f680 · 1f681 · 1f682 · 1f683 · 1f684 · 1f685 · 1f686 · 1f687 · 1f688 · 1f689 · 1f68a · 1f68b · 1f68c · 1f68d · 1f68e · 1f68f · 1f690 · 1f691 · 1f692 · 1f693 · 1f694 · 1f695 · 1f696 · 1f697 · 1f698 · 1f699 · 1f69a · 1f69b · 1f69c · 1f69d · 1f69e · 1f69f · 1f6a0 · 1f6a1 · 1f6a2 · 1f6a3 · 1f6a3-1f3fb · 1f6a3-1f3fb-200d-2640-fe0f · 1f6a3-1f3fb-200d-2642-fe0f · 1f6a3-1f3fc · 1f6a3-1f3fc-200d-2640-fe0f · 1f6a3-1f3fc-200d-2642-fe0f · 1f6a3-1f3fd · 1f6a3-1f3fd-200d-2640-fe0f · 1f6a3-1f3fd-200d-2642-fe0f · 1f6a3-1f3fe · 1f6a3-1f3fe-200d-2640-fe0f · 1f6a3-1f3fe-200d-2642-fe0f · 1f6a3-1f3ff · 1f6a3-1f3ff-200d-2640-fe0f · 1f6a3-1f3ff-200d-2642-fe0f · 1f6a3-200d-2640-fe0f · 1f6a3-200d-2642-fe0f · 1f6a4 · 1f6a5 · 1f6a6 · 1f6a7 · 1f6a8 · 1f6a9 · 1f6aa · 1f6ab · 1f6ac · 1f6ad · 1f6ae · 1f6af · 1f6b0 · 1f6b1 · 1f6b2 · 1f6b3 · 1f6b4 · 1f6b4-1f3fb · 1f6b4-1f3fb-200d-2640-fe0f · 1f6b4-1f3fb-200d-2642-fe0f · 1f6b4-1f3fc · 1f6b4-1f3fc-200d-2640-fe0f · 1f6b4-1f3fc-200d-2642-fe0f · 1f6b4-1f3fd · 1f6b4-1f3fd-200d-2640-fe0f · 1f6b4-1f3fd-200d-2642-fe0f · 1f6b4-1f3fe · 1f6b4-1f3fe-200d-2640-fe0f · 1f6b4-1f3fe-200d-2642-fe0f · 1f6b4-1f3ff · 1f6b4-1f3ff-200d-2640-fe0f · 1f6b4-1f3ff-200d-2642-fe0f · 1f6b4-200d-2640-fe0f · 1f6b4-200d-2642-fe0f · 1f6b5 · 1f6b5-1f3fb · 1f6b5-1f3fb-200d-2640-fe0f · 1f6b5-1f3fb-200d-2642-fe0f · 1f6b5-1f3fc · 1f6b5-1f3fc-200d-2640-fe0f · 1f6b5-1f3fc-200d-2642-fe0f · 1f6b5-1f3fd · 1f6b5-1f3fd-200d-2640-fe0f · 1f6b5-1f3fd-200d-2642-fe0f · 1f6b5-1f3fe · 1f6b5-1f3fe-200d-2640-fe0f · 1f6b5-1f3fe-200d-2642-fe0f · 1f6b5-1f3ff · 1f6b5-1f3ff-200d-2640-fe0f · 1f6b5-1f3ff-200d-2642-fe0f · 1f6b5-200d-2640-fe0f · 1f6b5-200d-2642-fe0f · 1f6b6 · 1f6b6-1f3fb · 1f6b6-1f3fb-200d-2640-fe0f · 1f6b6-1f3fb-200d-2640-fe0f-200d-27a1-fe0f · 1f6b6-1f3fb-200d-2642-fe0f · 1f6b6-1f3fb-200d-2642-fe0f-200d-27a1-fe0f · 1f6b6-1f3fb-200d-27a1-fe0f · 1f6b6-1f3fc · 1f6b6-1f3fc-200d-2640-fe0f · 1f6b6-1f3fc-200d-2640-fe0f-200d-27a1-fe0f · 1f6b6-1f3fc-200d-2642-fe0f · 1f6b6-1f3fc-200d-2642-fe0f-200d-27a1-fe0f · 1f6b6-1f3fc-200d-27a1-fe0f · 1f6b6-1f3fd · 1f6b6-1f3fd-200d-2640-fe0f · 1f6b6-1f3fd-200d-2640-fe0f-200d-27a1-fe0f · 1f6b6-1f3fd-200d-2642-fe0f · 1f6b6-1f3fd-200d-2642-fe0f-200d-27a1-fe0f · 1f6b6-1f3fd-200d-27a1-fe0f · 1f6b6-1f3fe · 1f6b6-1f3fe-200d-2640-fe0f · 1f6b6-1f3fe-200d-2640-fe0f-200d-27a1-fe0f · 1f6b6-1f3fe-200d-2642-fe0f · 1f6b6-1f3fe-200d-2642-fe0f-200d-27a1-fe0f · 1f6b6-1f3fe-200d-27a1-fe0f · 1f6b6-1f3ff · 1f6b6-1f3ff-200d-2640-fe0f · 1f6b6-1f3ff-200d-2640-fe0f-200d-27a1-fe0f · 1f6b6-1f3ff-200d-2642-fe0f · 1f6b6-1f3ff-200d-2642-fe0f-200d-27a1-fe0f · 1f6b6-1f3ff-200d-27a1-fe0f · 1f6b6-200d-2640-fe0f · 1f6b6-200d-2640-fe0f-200d-27a1-fe0f · 1f6b6-200d-2642-fe0f · 1f6b6-200d-2642-fe0f-200d-27a1-fe0f · 1f6b6-200d-27a1-fe0f · 1f6b7 · 1f6b8 · 1f6b9 · 1f6ba · 1f6bb · 1f6bc · 1f6bd · 1f6be · 1f6bf · 1f6c0 · 1f6c0-1f3fb · 1f6c0-1f3fc · 1f6c0-1f3fd · 1f6c0-1f3fe · 1f6c0-1f3ff · 1f6c1 · 1f6c2 · 1f6c3 · 1f6c4 · 1f6c5 · 1f6cb · 1f6cc · 1f6cc-1f3fb · 1f6cc-1f3fc · 1f6cc-1f3fd · 1f6cc-1f3fe · 1f6cc-1f3ff · 1f6cd · 1f6ce · 1f6cf · 1f6d0 · 1f6d1 · 1f6d2 · 1f6d5 · 1f6d6 · 1f6d7 · 1f6d8 · 1f6dc · 1f6dd · 1f6de · 1f6df · 1f6e0 · 1f6e1 · 1f6e2 · 1f6e3 · 1f6e4 · 1f6e5 · 1f6e9 · 1f6eb · 1f6ec · 1f6f0 · 1f6f3 · 1f6f4 · 1f6f5 · 1f6f6 · 1f6f7 · 1f6f8 · 1f6f9 · 1f6fa · 1f6fb · 1f6fc · 1f7e0 · 1f7e1 · 1f7e2 · 1f7e3 · 1f7e4 · 1f7e5 · 1f7e6 · 1f7e7 · 1f7e8 · 1f7e9 · 1f7ea · 1f7eb · 1f7f0 · 1f90c · 1f90c-1f3fb · 1f90c-1f3fc · 1f90c-1f3fd · 1f90c-1f3fe · 1f90c-1f3ff · 1f90d · 1f90e · 1f90f · 1f90f-1f3fb · 1f90f-1f3fc · 1f90f-1f3fd · 1f90f-1f3fe · 1f90f-1f3ff · 1f910 · 1f911 · 1f912 · 1f913 · 1f914 · 1f915 · 1f916 · 1f917 · 1f918 · 1f918-1f3fb · 1f918-1f3fc · 1f918-1f3fd · 1f918-1f3fe · 1f918-1f3ff · 1f919 · 1f919-1f3fb · 1f919-1f3fc · 1f919-1f3fd · 1f919-1f3fe · 1f919-1f3ff · 1f91a · 1f91a-1f3fb · 1f91a-1f3fc · 1f91a-1f3fd · 1f91a-1f3fe · 1f91a-1f3ff · 1f91b · 1f91b-1f3fb · 1f91b-1f3fc · 1f91b-1f3fd · 1f91b-1f3fe · 1f91b-1f3ff · 1f91c · 1f91c-1f3fb · 1f91c-1f3fc · 1f91c-1f3fd · 1f91c-1f3fe · 1f91c-1f3ff · 1f91d · 1f91d-1f3fb · 1f91d-1f3fc · 1f91d-1f3fd · 1f91d-1f3fe · 1f91d-1f3ff · 1f91e · 1f91e-1f3fb · 1f91e-1f3fc · 1f91e-1f3fd · 1f91e-1f3fe · 1f91e-1f3ff · 1f91f · 1f91f-1f3fb · 1f91f-1f3fc · 1f91f-1f3fd · 1f91f-1f3fe · 1f91f-1f3ff · 1f920 · 1f921 · 1f922 · 1f923 · 1f924 · 1f925 · 1f926 · 1f926-1f3fb · 1f926-1f3fb-200d-2640-fe0f · 1f926-1f3fb-200d-2642-fe0f · 1f926-1f3fc · 1f926-1f3fc-200d-2640-fe0f · 1f926-1f3fc-200d-2642-fe0f · 1f926-1f3fd · 1f926-1f3fd-200d-2640-fe0f · 1f926-1f3fd-200d-2642-fe0f · 1f926-1f3fe · 1f926-1f3fe-200d-2640-fe0f · 1f926-1f3fe-200d-2642-fe0f · 1f926-1f3ff · 1f926-1f3ff-200d-2640-fe0f · 1f926-1f3ff-200d-2642-fe0f · 1f926-200d-2640-fe0f · 1f926-200d-2642-fe0f · 1f927 · 1f928 · 1f929 · 1f92a · 1f92b · 1f92c · 1f92d · 1f92e · 1f92f · 1f930 · 1f930-1f3fb · 1f930-1f3fc · 1f930-1f3fd · 1f930-1f3fe · 1f930-1f3ff · 1f931 · 1f931-1f3fb · 1f931-1f3fc · 1f931-1f3fd · 1f931-1f3fe · 1f931-1f3ff · 1f932 · 1f932-1f3fb · 1f932-1f3fc · 1f932-1f3fd · 1f932-1f3fe · 1f932-1f3ff · 1f933 · 1f933-1f3fb · 1f933-1f3fc · 1f933-1f3fd · 1f933-1f3fe · 1f933-1f3ff · 1f934 · 1f934-1f3fb · 1f934-1f3fc · 1f934-1f3fd · 1f934-1f3fe · 1f934-1f3ff · 1f935 · 1f935-1f3fb · 1f935-1f3fb-200d-2640-fe0f · 1f935-1f3fb-200d-2642-fe0f · 1f935-1f3fc · 1f935-1f3fc-200d-2640-fe0f · 1f935-1f3fc-200d-2642-fe0f · 1f935-1f3fd · 1f935-1f3fd-200d-2640-fe0f · 1f935-1f3fd-200d-2642-fe0f · 1f935-1f3fe · 1f935-1f3fe-200d-2640-fe0f · 1f935-1f3fe-200d-2642-fe0f · 1f935-1f3ff · 1f935-1f3ff-200d-2640-fe0f · 1f935-1f3ff-200d-2642-fe0f · 1f935-200d-2640-fe0f · 1f935-200d-2642-fe0f · 1f936 · 1f936-1f3fb · 1f936-1f3fc · 1f936-1f3fd · 1f936-1f3fe · 1f936-1f3ff · 1f937 · 1f937-1f3fb · 1f937-1f3fb-200d-2640-fe0f · 1f937-1f3fb-200d-2642-fe0f · 1f937-1f3fc · 1f937-1f3fc-200d-2640-fe0f · 1f937-1f3fc-200d-2642-fe0f · 1f937-1f3fd · 1f937-1f3fd-200d-2640-fe0f · 1f937-1f3fd-200d-2642-fe0f · 1f937-1f3fe · 1f937-1f3fe-200d-2640-fe0f · 1f937-1f3fe-200d-2642-fe0f · 1f937-1f3ff · 1f937-1f3ff-200d-2640-fe0f · 1f937-1f3ff-200d-2642-fe0f · 1f937-200d-2640-fe0f · 1f937-200d-2642-fe0f · 1f938 · 1f938-1f3fb · 1f938-1f3fb-200d-2640-fe0f · 1f938-1f3fb-200d-2642-fe0f · 1f938-1f3fc · 1f938-1f3fc-200d-2640-fe0f · 1f938-1f3fc-200d-2642-fe0f · 1f938-1f3fd · 1f938-1f3fd-200d-2640-fe0f · 1f938-1f3fd-200d-2642-fe0f · 1f938-1f3fe · 1f938-1f3fe-200d-2640-fe0f · 1f938-1f3fe-200d-2642-fe0f · 1f938-1f3ff · 1f938-1f3ff-200d-2640-fe0f · 1f938-1f3ff-200d-2642-fe0f · 1f938-200d-2640-fe0f · 1f938-200d-2642-fe0f · 1f939 · 1f939-1f3fb · 1f939-1f3fb-200d-2640-fe0f · 1f939-1f3fb-200d-2642-fe0f · 1f939-1f3fc · 1f939-1f3fc-200d-2640-fe0f · 1f939-1f3fc-200d-2642-fe0f · 1f939-1f3fd · 1f939-1f3fd-200d-2640-fe0f · 1f939-1f3fd-200d-2642-fe0f · 1f939-1f3fe · 1f939-1f3fe-200d-2640-fe0f · 1f939-1f3fe-200d-2642-fe0f · 1f939-1f3ff · 1f939-1f3ff-200d-2640-fe0f · 1f939-1f3ff-200d-2642-fe0f · 1f939-200d-2640-fe0f · 1f939-200d-2642-fe0f · 1f93a · 1f93c · 1f93c-1f3fb · 1f93c-1f3fb-200d-2640-fe0f · 1f93c-1f3fb-200d-2642-fe0f · 1f93c-1f3fc · 1f93c-1f3fc-200d-2640-fe0f · 1f93c-1f3fc-200d-2642-fe0f · 1f93c-1f3fd · 1f93c-1f3fd-200d-2640-fe0f · 1f93c-1f3fd-200d-2642-fe0f · 1f93c-1f3fe · 1f93c-1f3fe-200d-2640-fe0f · 1f93c-1f3fe-200d-2642-fe0f · 1f93c-1f3ff · 1f93c-1f3ff-200d-2640-fe0f · 1f93c-1f3ff-200d-2642-fe0f · 1f93c-200d-2640-fe0f · 1f93c-200d-2642-fe0f · 1f93d · 1f93d-1f3fb · 1f93d-1f3fb-200d-2640-fe0f · 1f93d-1f3fb-200d-2642-fe0f · 1f93d-1f3fc · 1f93d-1f3fc-200d-2640-fe0f · 1f93d-1f3fc-200d-2642-fe0f · 1f93d-1f3fd · 1f93d-1f3fd-200d-2640-fe0f · 1f93d-1f3fd-200d-2642-fe0f · 1f93d-1f3fe · 1f93d-1f3fe-200d-2640-fe0f · 1f93d-1f3fe-200d-2642-fe0f · 1f93d-1f3ff · 1f93d-1f3ff-200d-2640-fe0f · 1f93d-1f3ff-200d-2642-fe0f · 1f93d-200d-2640-fe0f · 1f93d-200d-2642-fe0f · 1f93e · 1f93e-1f3fb · 1f93e-1f3fb-200d-2640-fe0f · 1f93e-1f3fb-200d-2642-fe0f · 1f93e-1f3fc · 1f93e-1f3fc-200d-2640-fe0f · 1f93e-1f3fc-200d-2642-fe0f · 1f93e-1f3fd · 1f93e-1f3fd-200d-2640-fe0f · 1f93e-1f3fd-200d-2642-fe0f · 1f93e-1f3fe · 1f93e-1f3fe-200d-2640-fe0f · 1f93e-1f3fe-200d-2642-fe0f · 1f93e-1f3ff · 1f93e-1f3ff-200d-2640-fe0f · 1f93e-1f3ff-200d-2642-fe0f · 1f93e-200d-2640-fe0f · 1f93e-200d-2642-fe0f · 1f93f · 1f940 · 1f941 · 1f942 · 1f943 · 1f944 · 1f945 · 1f947 · 1f948 · 1f949 · 1f94a · 1f94b · 1f94c · 1f94d · 1f94e · 1f94f · 1f950 · 1f951 · 1f952 · 1f953 · 1f954 · 1f955 · 1f956 · 1f957 · 1f958 · 1f959 · 1f95a · 1f95b · 1f95c · 1f95d · 1f95e · 1f95f · 1f960 · 1f961 · 1f962 · 1f963 · 1f964 · 1f965 · 1f966 · 1f967 · 1f968 · 1f969 · 1f96a · 1f96b · 1f96c · 1f96d · 1f96e · 1f96f · 1f970 · 1f971 · 1f972 · 1f973 · 1f974 · 1f975 · 1f976 · 1f977 · 1f977-1f3fb · 1f977-1f3fc · 1f977-1f3fd · 1f977-1f3fe · 1f977-1f3ff · 1f978 · 1f979 · 1f97a · 1f97b · 1f97c · 1f97d · 1f97e · 1f97f · 1f980 · 1f981 · 1f982 · 1f983 · 1f984 · 1f985 · 1f986 · 1f987 · 1f988 · 1f989 · 1f98a · 1f98b · 1f98c · 1f98d · 1f98e · 1f98f · 1f990 · 1f991 · 1f992 · 1f993 · 1f994 · 1f995 · 1f996 · 1f997 · 1f998 · 1f999 · 1f99a · 1f99b · 1f99c · 1f99d · 1f99e · 1f99f · 1f9a0 · 1f9a1 · 1f9a2 · 1f9a3 · 1f9a4 · 1f9a5 · 1f9a6 · 1f9a7 · 1f9a8 · 1f9a9 · 1f9aa · 1f9ab · 1f9ac · 1f9ad · 1f9ae · 1f9af · 1f9b0 · 1f9b1 · 1f9b2 · 1f9b3 · 1f9b4 · 1f9b5 · 1f9b5-1f3fb · 1f9b5-1f3fc · 1f9b5-1f3fd · 1f9b5-1f3fe · 1f9b5-1f3ff · 1f9b6 · 1f9b6-1f3fb · 1f9b6-1f3fc · 1f9b6-1f3fd · 1f9b6-1f3fe · 1f9b6-1f3ff · 1f9b7 · 1f9b8 · 1f9b8-1f3fb · 1f9b8-1f3fb-200d-2640-fe0f · 1f9b8-1f3fb-200d-2642-fe0f · 1f9b8-1f3fc · 1f9b8-1f3fc-200d-2640-fe0f · 1f9b8-1f3fc-200d-2642-fe0f · 1f9b8-1f3fd · 1f9b8-1f3fd-200d-2640-fe0f · 1f9b8-1f3fd-200d-2642-fe0f · 1f9b8-1f3fe · 1f9b8-1f3fe-200d-2640-fe0f · 1f9b8-1f3fe-200d-2642-fe0f · 1f9b8-1f3ff · 1f9b8-1f3ff-200d-2640-fe0f · 1f9b8-1f3ff-200d-2642-fe0f · 1f9b8-200d-2640-fe0f · 1f9b8-200d-2642-fe0f · 1f9b9 · 1f9b9-1f3fb · 1f9b9-1f3fb-200d-2640-fe0f · 1f9b9-1f3fb-200d-2642-fe0f · 1f9b9-1f3fc · 1f9b9-1f3fc-200d-2640-fe0f · 1f9b9-1f3fc-200d-2642-fe0f · 1f9b9-1f3fd · 1f9b9-1f3fd-200d-2640-fe0f · 1f9b9-1f3fd-200d-2642-fe0f · 1f9b9-1f3fe · 1f9b9-1f3fe-200d-2640-fe0f · 1f9b9-1f3fe-200d-2642-fe0f · 1f9b9-1f3ff · 1f9b9-1f3ff-200d-2640-fe0f · 1f9b9-1f3ff-200d-2642-fe0f · 1f9b9-200d-2640-fe0f · 1f9b9-200d-2642-fe0f · 1f9ba · 1f9bb · 1f9bb-1f3fb · 1f9bb-1f3fc · 1f9bb-1f3fd · 1f9bb-1f3fe · 1f9bb-1f3ff · 1f9bc · 1f9bd · 1f9be · 1f9bf · 1f9c0 · 1f9c1 · 1f9c2 · 1f9c3 · 1f9c4 · 1f9c5 · 1f9c6 · 1f9c7 · 1f9c8 · 1f9c9 · 1f9ca · 1f9cb · 1f9cc · 1f9cd · 1f9cd-1f3fb · 1f9cd-1f3fb-200d-2640-fe0f · 1f9cd-1f3fb-200d-2642-fe0f · 1f9cd-1f3fc · 1f9cd-1f3fc-200d-2640-fe0f · 1f9cd-1f3fc-200d-2642-fe0f · 1f9cd-1f3fd · 1f9cd-1f3fd-200d-2640-fe0f · 1f9cd-1f3fd-200d-2642-fe0f · 1f9cd-1f3fe · 1f9cd-1f3fe-200d-2640-fe0f · 1f9cd-1f3fe-200d-2642-fe0f · 1f9cd-1f3ff · 1f9cd-1f3ff-200d-2640-fe0f · 1f9cd-1f3ff-200d-2642-fe0f · 1f9cd-200d-2640-fe0f · 1f9cd-200d-2642-fe0f · 1f9ce · 1f9ce-1f3fb · 1f9ce-1f3fb-200d-2640-fe0f · 1f9ce-1f3fb-200d-2640-fe0f-200d-27a1-fe0f · 1f9ce-1f3fb-200d-2642-fe0f · 1f9ce-1f3fb-200d-2642-fe0f-200d-27a1-fe0f · 1f9ce-1f3fb-200d-27a1-fe0f · 1f9ce-1f3fc · 1f9ce-1f3fc-200d-2640-fe0f · 1f9ce-1f3fc-200d-2640-fe0f-200d-27a1-fe0f · 1f9ce-1f3fc-200d-2642-fe0f · 1f9ce-1f3fc-200d-2642-fe0f-200d-27a1-fe0f · 1f9ce-1f3fc-200d-27a1-fe0f · 1f9ce-1f3fd · 1f9ce-1f3fd-200d-2640-fe0f · 1f9ce-1f3fd-200d-2640-fe0f-200d-27a1-fe0f · 1f9ce-1f3fd-200d-2642-fe0f · 1f9ce-1f3fd-200d-2642-fe0f-200d-27a1-fe0f · 1f9ce-1f3fd-200d-27a1-fe0f · 1f9ce-1f3fe · 1f9ce-1f3fe-200d-2640-fe0f · 1f9ce-1f3fe-200d-2640-fe0f-200d-27a1-fe0f · 1f9ce-1f3fe-200d-2642-fe0f · 1f9ce-1f3fe-200d-2642-fe0f-200d-27a1-fe0f · 1f9ce-1f3fe-200d-27a1-fe0f · 1f9ce-1f3ff · 1f9ce-1f3ff-200d-2640-fe0f · 1f9ce-1f3ff-200d-2640-fe0f-200d-27a1-fe0f · 1f9ce-1f3ff-200d-2642-fe0f · 1f9ce-1f3ff-200d-2642-fe0f-200d-27a1-fe0f · 1f9ce-1f3ff-200d-27a1-fe0f · 1f9ce-200d-2640-fe0f · 1f9ce-200d-2640-fe0f-200d-27a1-fe0f · 1f9ce-200d-2642-fe0f · 1f9ce-200d-2642-fe0f-200d-27a1-fe0f · 1f9ce-200d-27a1-fe0f · 1f9cf · 1f9cf-1f3fb · 1f9cf-1f3fb-200d-2640-fe0f · 1f9cf-1f3fb-200d-2642-fe0f · 1f9cf-1f3fc · 1f9cf-1f3fc-200d-2640-fe0f · 1f9cf-1f3fc-200d-2642-fe0f · 1f9cf-1f3fd · 1f9cf-1f3fd-200d-2640-fe0f · 1f9cf-1f3fd-200d-2642-fe0f · 1f9cf-1f3fe · 1f9cf-1f3fe-200d-2640-fe0f · 1f9cf-1f3fe-200d-2642-fe0f · 1f9cf-1f3ff · 1f9cf-1f3ff-200d-2640-fe0f · 1f9cf-1f3ff-200d-2642-fe0f · 1f9cf-200d-2640-fe0f · 1f9cf-200d-2642-fe0f · 1f9d0 · 1f9d1 · 1f9d1-1f3fb · 1f9d1-1f3fb-200d-1f33e · 1f9d1-1f3fb-200d-1f373 · 1f9d1-1f3fb-200d-1f37c · 1f9d1-1f3fb-200d-1f384 · 1f9d1-1f3fb-200d-1f393 · 1f9d1-1f3fb-200d-1f3a4 · 1f9d1-1f3fb-200d-1f3a8 · 1f9d1-1f3fb-200d-1f3eb · 1f9d1-1f3fb-200d-1f3ed · 1f9d1-1f3fb-200d-1f430-200d-1f9d1-1f3fc · 1f9d1-1f3fb-200d-1f430-200d-1f9d1-1f3fd · 1f9d1-1f3fb-200d-1f430-200d-1f9d1-1f3fe · 1f9d1-1f3fb-200d-1f430-200d-1f9d1-1f3ff · 1f9d1-1f3fb-200d-1f4bb · 1f9d1-1f3fb-200d-1f4bc · 1f9d1-1f3fb-200d-1f527 · 1f9d1-1f3fb-200d-1f52c · 1f9d1-1f3fb-200d-1f680 · 1f9d1-1f3fb-200d-1f692 · 1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fb · 1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fc · 1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fd · 1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fe · 1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3ff · 1f9d1-1f3fb-200d-1f9af · 1f9d1-1f3fb-200d-1f9af-200d-27a1-fe0f · 1f9d1-1f3fb-200d-1f9b0 · 1f9d1-1f3fb-200d-1f9b1 · 1f9d1-1f3fb-200d-1f9b2 · 1f9d1-1f3fb-200d-1f9b3 · 1f9d1-1f3fb-200d-1f9bc · 1f9d1-1f3fb-200d-1f9bc-200d-27a1-fe0f · 1f9d1-1f3fb-200d-1f9bd · 1f9d1-1f3fb-200d-1f9bd-200d-27a1-fe0f · 1f9d1-1f3fb-200d-1fa70 · 1f9d1-1f3fb-200d-1faef-200d-1f9d1-1f3fc · 1f9d1-1f3fb-200d-1faef-200d-1f9d1-1f3fd · 1f9d1-1f3fb-200d-1faef-200d-1f9d1-1f3fe · 1f9d1-1f3fb-200d-1faef-200d-1f9d1-1f3ff · 1f9d1-1f3fb-200d-2695-fe0f · 1f9d1-1f3fb-200d-2696-fe0f · 1f9d1-1f3fb-200d-2708-fe0f · 1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc · 1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd · 1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe · 1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff · 1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fc · 1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fd · 1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fe · 1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3ff · 1f9d1-1f3fc · 1f9d1-1f3fc-200d-1f33e · 1f9d1-1f3fc-200d-1f373 · 1f9d1-1f3fc-200d-1f37c · 1f9d1-1f3fc-200d-1f384 · 1f9d1-1f3fc-200d-1f393 · 1f9d1-1f3fc-200d-1f3a4 · 1f9d1-1f3fc-200d-1f3a8 · 1f9d1-1f3fc-200d-1f3eb · 1f9d1-1f3fc-200d-1f3ed · 1f9d1-1f3fc-200d-1f430-200d-1f9d1-1f3fb · 1f9d1-1f3fc-200d-1f430-200d-1f9d1-1f3fd · 1f9d1-1f3fc-200d-1f430-200d-1f9d1-1f3fe · 1f9d1-1f3fc-200d-1f430-200d-1f9d1-1f3ff · 1f9d1-1f3fc-200d-1f4bb · 1f9d1-1f3fc-200d-1f4bc · 1f9d1-1f3fc-200d-1f527 · 1f9d1-1f3fc-200d-1f52c · 1f9d1-1f3fc-200d-1f680 · 1f9d1-1f3fc-200d-1f692 · 1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fb · 1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fc · 1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fd · 1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fe · 1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3ff · 1f9d1-1f3fc-200d-1f9af · 1f9d1-1f3fc-200d-1f9af-200d-27a1-fe0f · 1f9d1-1f3fc-200d-1f9b0 · 1f9d1-1f3fc-200d-1f9b1 · 1f9d1-1f3fc-200d-1f9b2 · 1f9d1-1f3fc-200d-1f9b3 · 1f9d1-1f3fc-200d-1f9bc · 1f9d1-1f3fc-200d-1f9bc-200d-27a1-fe0f · 1f9d1-1f3fc-200d-1f9bd · 1f9d1-1f3fc-200d-1f9bd-200d-27a1-fe0f · 1f9d1-1f3fc-200d-1fa70 · 1f9d1-1f3fc-200d-1faef-200d-1f9d1-1f3fb · 1f9d1-1f3fc-200d-1faef-200d-1f9d1-1f3fd · 1f9d1-1f3fc-200d-1faef-200d-1f9d1-1f3fe · 1f9d1-1f3fc-200d-1faef-200d-1f9d1-1f3ff · 1f9d1-1f3fc-200d-2695-fe0f · 1f9d1-1f3fc-200d-2696-fe0f · 1f9d1-1f3fc-200d-2708-fe0f · 1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb · 1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd · 1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe · 1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff · 1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fb · 1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fd · 1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fe · 1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3ff · 1f9d1-1f3fd · 1f9d1-1f3fd-200d-1f33e · 1f9d1-1f3fd-200d-1f373 · 1f9d1-1f3fd-200d-1f37c · 1f9d1-1f3fd-200d-1f384 · 1f9d1-1f3fd-200d-1f393 · 1f9d1-1f3fd-200d-1f3a4 · 1f9d1-1f3fd-200d-1f3a8 · 1f9d1-1f3fd-200d-1f3eb · 1f9d1-1f3fd-200d-1f3ed · 1f9d1-1f3fd-200d-1f430-200d-1f9d1-1f3fb · 1f9d1-1f3fd-200d-1f430-200d-1f9d1-1f3fc · 1f9d1-1f3fd-200d-1f430-200d-1f9d1-1f3fe · 1f9d1-1f3fd-200d-1f430-200d-1f9d1-1f3ff · 1f9d1-1f3fd-200d-1f4bb · 1f9d1-1f3fd-200d-1f4bc · 1f9d1-1f3fd-200d-1f527 · 1f9d1-1f3fd-200d-1f52c · 1f9d1-1f3fd-200d-1f680 · 1f9d1-1f3fd-200d-1f692 · 1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fb · 1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fc · 1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fd · 1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fe · 1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3ff · 1f9d1-1f3fd-200d-1f9af · 1f9d1-1f3fd-200d-1f9af-200d-27a1-fe0f · 1f9d1-1f3fd-200d-1f9b0 · 1f9d1-1f3fd-200d-1f9b1 · 1f9d1-1f3fd-200d-1f9b2 · 1f9d1-1f3fd-200d-1f9b3 · 1f9d1-1f3fd-200d-1f9bc · 1f9d1-1f3fd-200d-1f9bc-200d-27a1-fe0f · 1f9d1-1f3fd-200d-1f9bd · 1f9d1-1f3fd-200d-1f9bd-200d-27a1-fe0f · 1f9d1-1f3fd-200d-1fa70 · 1f9d1-1f3fd-200d-1faef-200d-1f9d1-1f3fb · 1f9d1-1f3fd-200d-1faef-200d-1f9d1-1f3fc · 1f9d1-1f3fd-200d-1faef-200d-1f9d1-1f3fe · 1f9d1-1f3fd-200d-1faef-200d-1f9d1-1f3ff · 1f9d1-1f3fd-200d-2695-fe0f · 1f9d1-1f3fd-200d-2696-fe0f · 1f9d1-1f3fd-200d-2708-fe0f · 1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb · 1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc · 1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe · 1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff · 1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fb · 1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fc · 1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fe · 1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3ff · 1f9d1-1f3fe · 1f9d1-1f3fe-200d-1f33e · 1f9d1-1f3fe-200d-1f373 · 1f9d1-1f3fe-200d-1f37c · 1f9d1-1f3fe-200d-1f384 · 1f9d1-1f3fe-200d-1f393 · 1f9d1-1f3fe-200d-1f3a4 · 1f9d1-1f3fe-200d-1f3a8 · 1f9d1-1f3fe-200d-1f3eb · 1f9d1-1f3fe-200d-1f3ed · 1f9d1-1f3fe-200d-1f430-200d-1f9d1-1f3fb · 1f9d1-1f3fe-200d-1f430-200d-1f9d1-1f3fc · 1f9d1-1f3fe-200d-1f430-200d-1f9d1-1f3fd · 1f9d1-1f3fe-200d-1f430-200d-1f9d1-1f3ff · 1f9d1-1f3fe-200d-1f4bb · 1f9d1-1f3fe-200d-1f4bc · 1f9d1-1f3fe-200d-1f527 · 1f9d1-1f3fe-200d-1f52c · 1f9d1-1f3fe-200d-1f680 · 1f9d1-1f3fe-200d-1f692 · 1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fb · 1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fc · 1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fd · 1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fe · 1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3ff · 1f9d1-1f3fe-200d-1f9af · 1f9d1-1f3fe-200d-1f9af-200d-27a1-fe0f · 1f9d1-1f3fe-200d-1f9b0 · 1f9d1-1f3fe-200d-1f9b1 · 1f9d1-1f3fe-200d-1f9b2 · 1f9d1-1f3fe-200d-1f9b3 · 1f9d1-1f3fe-200d-1f9bc · 1f9d1-1f3fe-200d-1f9bc-200d-27a1-fe0f · 1f9d1-1f3fe-200d-1f9bd · 1f9d1-1f3fe-200d-1f9bd-200d-27a1-fe0f · 1f9d1-1f3fe-200d-1fa70 · 1f9d1-1f3fe-200d-1faef-200d-1f9d1-1f3fb · 1f9d1-1f3fe-200d-1faef-200d-1f9d1-1f3fc · 1f9d1-1f3fe-200d-1faef-200d-1f9d1-1f3fd · 1f9d1-1f3fe-200d-1faef-200d-1f9d1-1f3ff · 1f9d1-1f3fe-200d-2695-fe0f · 1f9d1-1f3fe-200d-2696-fe0f · 1f9d1-1f3fe-200d-2708-fe0f · 1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb · 1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc · 1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd · 1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff · 1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fb · 1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fc · 1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fd · 1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3ff · 1f9d1-1f3ff · 1f9d1-1f3ff-200d-1f33e · 1f9d1-1f3ff-200d-1f373 · 1f9d1-1f3ff-200d-1f37c · 1f9d1-1f3ff-200d-1f384 · 1f9d1-1f3ff-200d-1f393 · 1f9d1-1f3ff-200d-1f3a4 · 1f9d1-1f3ff-200d-1f3a8 · 1f9d1-1f3ff-200d-1f3eb · 1f9d1-1f3ff-200d-1f3ed · 1f9d1-1f3ff-200d-1f430-200d-1f9d1-1f3fb · 1f9d1-1f3ff-200d-1f430-200d-1f9d1-1f3fc · 1f9d1-1f3ff-200d-1f430-200d-1f9d1-1f3fd · 1f9d1-1f3ff-200d-1f430-200d-1f9d1-1f3fe · 1f9d1-1f3ff-200d-1f4bb · 1f9d1-1f3ff-200d-1f4bc · 1f9d1-1f3ff-200d-1f527 · 1f9d1-1f3ff-200d-1f52c · 1f9d1-1f3ff-200d-1f680 · 1f9d1-1f3ff-200d-1f692 · 1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fb · 1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fc · 1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fd · 1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fe · 1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3ff · 1f9d1-1f3ff-200d-1f9af · 1f9d1-1f3ff-200d-1f9af-200d-27a1-fe0f · 1f9d1-1f3ff-200d-1f9b0 · 1f9d1-1f3ff-200d-1f9b1 · 1f9d1-1f3ff-200d-1f9b2 · 1f9d1-1f3ff-200d-1f9b3 · 1f9d1-1f3ff-200d-1f9bc · 1f9d1-1f3ff-200d-1f9bc-200d-27a1-fe0f · 1f9d1-1f3ff-200d-1f9bd · 1f9d1-1f3ff-200d-1f9bd-200d-27a1-fe0f · 1f9d1-1f3ff-200d-1fa70 · 1f9d1-1f3ff-200d-1faef-200d-1f9d1-1f3fb · 1f9d1-1f3ff-200d-1faef-200d-1f9d1-1f3fc · 1f9d1-1f3ff-200d-1faef-200d-1f9d1-1f3fd · 1f9d1-1f3ff-200d-1faef-200d-1f9d1-1f3fe · 1f9d1-1f3ff-200d-2695-fe0f · 1f9d1-1f3ff-200d-2696-fe0f · 1f9d1-1f3ff-200d-2708-fe0f · 1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb · 1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc · 1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd · 1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe · 1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fb · 1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fc · 1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fd · 1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fe · 1f9d1-200d-1f33e · 1f9d1-200d-1f373 · 1f9d1-200d-1f37c · 1f9d1-200d-1f384 · 1f9d1-200d-1f393 · 1f9d1-200d-1f3a4 · 1f9d1-200d-1f3a8 · 1f9d1-200d-1f3eb · 1f9d1-200d-1f3ed · 1f9d1-200d-1f4bb · 1f9d1-200d-1f4bc · 1f9d1-200d-1f527 · 1f9d1-200d-1f52c · 1f9d1-200d-1f680 · 1f9d1-200d-1f692 · 1f9d1-200d-1f91d-200d-1f9d1 · 1f9d1-200d-1f9af · 1f9d1-200d-1f9af-200d-27a1-fe0f · 1f9d1-200d-1f9b0 · 1f9d1-200d-1f9b1 · 1f9d1-200d-1f9b2 · 1f9d1-200d-1f9b3 · 1f9d1-200d-1f9bc · 1f9d1-200d-1f9bc-200d-27a1-fe0f · 1f9d1-200d-1f9bd · 1f9d1-200d-1f9bd-200d-27a1-fe0f · 1f9d1-200d-1f9d1-200d-1f9d2 · 1f9d1-200d-1f9d1-200d-1f9d2-200d-1f9d2 · 1f9d1-200d-1f9d2 · 1f9d1-200d-1f9d2-200d-1f9d2 · 1f9d1-200d-1fa70 · 1f9d1-200d-2695-fe0f · 1f9d1-200d-2696-fe0f · 1f9d1-200d-2708-fe0f · 1f9d2 · 1f9d2-1f3fb · 1f9d2-1f3fc · 1f9d2-1f3fd · 1f9d2-1f3fe · 1f9d2-1f3ff · 1f9d3 · 1f9d3-1f3fb · 1f9d3-1f3fc · 1f9d3-1f3fd · 1f9d3-1f3fe · 1f9d3-1f3ff · 1f9d4 · 1f9d4-1f3fb · 1f9d4-1f3fb-200d-2640-fe0f · 1f9d4-1f3fb-200d-2642-fe0f · 1f9d4-1f3fc · 1f9d4-1f3fc-200d-2640-fe0f · 1f9d4-1f3fc-200d-2642-fe0f · 1f9d4-1f3fd · 1f9d4-1f3fd-200d-2640-fe0f · 1f9d4-1f3fd-200d-2642-fe0f · 1f9d4-1f3fe · 1f9d4-1f3fe-200d-2640-fe0f · 1f9d4-1f3fe-200d-2642-fe0f · 1f9d4-1f3ff · 1f9d4-1f3ff-200d-2640-fe0f · 1f9d4-1f3ff-200d-2642-fe0f · 1f9d4-200d-2640-fe0f · 1f9d4-200d-2642-fe0f · 1f9d5 · 1f9d5-1f3fb · 1f9d5-1f3fc · 1f9d5-1f3fd · 1f9d5-1f3fe · 1f9d5-1f3ff · 1f9d6 · 1f9d6-1f3fb · 1f9d6-1f3fb-200d-2640-fe0f · 1f9d6-1f3fb-200d-2642-fe0f · 1f9d6-1f3fc · 1f9d6-1f3fc-200d-2640-fe0f · 1f9d6-1f3fc-200d-2642-fe0f · 1f9d6-1f3fd · 1f9d6-1f3fd-200d-2640-fe0f · 1f9d6-1f3fd-200d-2642-fe0f · 1f9d6-1f3fe · 1f9d6-1f3fe-200d-2640-fe0f · 1f9d6-1f3fe-200d-2642-fe0f · 1f9d6-1f3ff · 1f9d6-1f3ff-200d-2640-fe0f · 1f9d6-1f3ff-200d-2642-fe0f · 1f9d6-200d-2640-fe0f · 1f9d6-200d-2642-fe0f · 1f9d7 · 1f9d7-1f3fb · 1f9d7-1f3fb-200d-2640-fe0f · 1f9d7-1f3fb-200d-2642-fe0f · 1f9d7-1f3fc · 1f9d7-1f3fc-200d-2640-fe0f · 1f9d7-1f3fc-200d-2642-fe0f · 1f9d7-1f3fd · 1f9d7-1f3fd-200d-2640-fe0f · 1f9d7-1f3fd-200d-2642-fe0f · 1f9d7-1f3fe · 1f9d7-1f3fe-200d-2640-fe0f · 1f9d7-1f3fe-200d-2642-fe0f · 1f9d7-1f3ff · 1f9d7-1f3ff-200d-2640-fe0f · 1f9d7-1f3ff-200d-2642-fe0f · 1f9d7-200d-2640-fe0f · 1f9d7-200d-2642-fe0f · 1f9d8 · 1f9d8-1f3fb · 1f9d8-1f3fb-200d-2640-fe0f · 1f9d8-1f3fb-200d-2642-fe0f · 1f9d8-1f3fc · 1f9d8-1f3fc-200d-2640-fe0f · 1f9d8-1f3fc-200d-2642-fe0f · 1f9d8-1f3fd · 1f9d8-1f3fd-200d-2640-fe0f · 1f9d8-1f3fd-200d-2642-fe0f · 1f9d8-1f3fe · 1f9d8-1f3fe-200d-2640-fe0f · 1f9d8-1f3fe-200d-2642-fe0f · 1f9d8-1f3ff · 1f9d8-1f3ff-200d-2640-fe0f · 1f9d8-1f3ff-200d-2642-fe0f · 1f9d8-200d-2640-fe0f · 1f9d8-200d-2642-fe0f · 1f9d9 · 1f9d9-1f3fb · 1f9d9-1f3fb-200d-2640-fe0f · 1f9d9-1f3fb-200d-2642-fe0f · 1f9d9-1f3fc · 1f9d9-1f3fc-200d-2640-fe0f · 1f9d9-1f3fc-200d-2642-fe0f · 1f9d9-1f3fd · 1f9d9-1f3fd-200d-2640-fe0f · 1f9d9-1f3fd-200d-2642-fe0f · 1f9d9-1f3fe · 1f9d9-1f3fe-200d-2640-fe0f · 1f9d9-1f3fe-200d-2642-fe0f · 1f9d9-1f3ff · 1f9d9-1f3ff-200d-2640-fe0f · 1f9d9-1f3ff-200d-2642-fe0f · 1f9d9-200d-2640-fe0f · 1f9d9-200d-2642-fe0f · 1f9da · 1f9da-1f3fb · 1f9da-1f3fb-200d-2640-fe0f · 1f9da-1f3fb-200d-2642-fe0f · 1f9da-1f3fc · 1f9da-1f3fc-200d-2640-fe0f · 1f9da-1f3fc-200d-2642-fe0f · 1f9da-1f3fd · 1f9da-1f3fd-200d-2640-fe0f · 1f9da-1f3fd-200d-2642-fe0f · 1f9da-1f3fe · 1f9da-1f3fe-200d-2640-fe0f · 1f9da-1f3fe-200d-2642-fe0f · 1f9da-1f3ff · 1f9da-1f3ff-200d-2640-fe0f · 1f9da-1f3ff-200d-2642-fe0f · 1f9da-200d-2640-fe0f · 1f9da-200d-2642-fe0f · 1f9db · 1f9db-1f3fb · 1f9db-1f3fb-200d-2640-fe0f · 1f9db-1f3fb-200d-2642-fe0f · 1f9db-1f3fc · 1f9db-1f3fc-200d-2640-fe0f · 1f9db-1f3fc-200d-2642-fe0f · 1f9db-1f3fd · 1f9db-1f3fd-200d-2640-fe0f · 1f9db-1f3fd-200d-2642-fe0f · 1f9db-1f3fe · 1f9db-1f3fe-200d-2640-fe0f · 1f9db-1f3fe-200d-2642-fe0f · 1f9db-1f3ff · 1f9db-1f3ff-200d-2640-fe0f · 1f9db-1f3ff-200d-2642-fe0f · 1f9db-200d-2640-fe0f · 1f9db-200d-2642-fe0f · 1f9dc · 1f9dc-1f3fb · 1f9dc-1f3fb-200d-2640-fe0f · 1f9dc-1f3fb-200d-2642-fe0f · 1f9dc-1f3fc · 1f9dc-1f3fc-200d-2640-fe0f · 1f9dc-1f3fc-200d-2642-fe0f · 1f9dc-1f3fd · 1f9dc-1f3fd-200d-2640-fe0f · 1f9dc-1f3fd-200d-2642-fe0f · 1f9dc-1f3fe · 1f9dc-1f3fe-200d-2640-fe0f · 1f9dc-1f3fe-200d-2642-fe0f · 1f9dc-1f3ff · 1f9dc-1f3ff-200d-2640-fe0f · 1f9dc-1f3ff-200d-2642-fe0f · 1f9dc-200d-2640-fe0f · 1f9dc-200d-2642-fe0f · 1f9dd · 1f9dd-1f3fb · 1f9dd-1f3fb-200d-2640-fe0f · 1f9dd-1f3fb-200d-2642-fe0f · 1f9dd-1f3fc · 1f9dd-1f3fc-200d-2640-fe0f · 1f9dd-1f3fc-200d-2642-fe0f · 1f9dd-1f3fd · 1f9dd-1f3fd-200d-2640-fe0f · 1f9dd-1f3fd-200d-2642-fe0f · 1f9dd-1f3fe · 1f9dd-1f3fe-200d-2640-fe0f · 1f9dd-1f3fe-200d-2642-fe0f · 1f9dd-1f3ff · 1f9dd-1f3ff-200d-2640-fe0f · 1f9dd-1f3ff-200d-2642-fe0f · 1f9dd-200d-2640-fe0f · 1f9dd-200d-2642-fe0f · 1f9de · 1f9de-200d-2640-fe0f · 1f9de-200d-2642-fe0f · 1f9df · 1f9df-200d-2640-fe0f · 1f9df-200d-2642-fe0f · 1f9e0 · 1f9e1 · 1f9e2 · 1f9e3 · 1f9e4 · 1f9e5 · 1f9e6 · 1f9e7 · 1f9e8 · 1f9e9 · 1f9ea · 1f9eb · 1f9ec · 1f9ed · 1f9ee · 1f9ef · 1f9f0 · 1f9f1 · 1f9f2 · 1f9f3 · 1f9f4 · 1f9f5 · 1f9f6 · 1f9f7 · 1f9f8 · 1f9f9 · 1f9fa · 1f9fb · 1f9fc · 1f9fd · 1f9fe · 1f9ff · 1fa70 · 1fa71 · 1fa72 · 1fa73 · 1fa74 · 1fa75 · 1fa76 · 1fa77 · 1fa78 · 1fa79 · 1fa7a · 1fa7b · 1fa7c · 1fa80 · 1fa81 · 1fa82 · 1fa83 · 1fa84 · 1fa85 · 1fa86 · 1fa87 · 1fa88 · 1fa89 · 1fa8a · 1fa8e · 1fa8f · 1fa90 · 1fa91 · 1fa92 · 1fa93 · 1fa94 · 1fa95 · 1fa96 · 1fa97 · 1fa98 · 1fa99 · 1fa9a · 1fa9b · 1fa9c · 1fa9d · 1fa9e · 1fa9f · 1faa0 · 1faa1 · 1faa2 · 1faa3 · 1faa4 · 1faa5 · 1faa6 · 1faa7 · 1faa8 · 1faa9 · 1faaa · 1faab · 1faac · 1faad · 1faae · 1faaf · 1fab0 · 1fab1 · 1fab2 · 1fab3 · 1fab4 · 1fab5 · 1fab6 · 1fab7 · 1fab8 · 1fab9 · 1faba · 1fabb · 1fabc · 1fabd · 1fabe · 1fabf · 1fac0 · 1fac1 · 1fac2 · 1fac3 · 1fac3-1f3fb · 1fac3-1f3fc · 1fac3-1f3fd · 1fac3-1f3fe · 1fac3-1f3ff · 1fac4 · 1fac4-1f3fb · 1fac4-1f3fc · 1fac4-1f3fd · 1fac4-1f3fe · 1fac4-1f3ff · 1fac5 · 1fac5-1f3fb · 1fac5-1f3fc · 1fac5-1f3fd · 1fac5-1f3fe · 1fac5-1f3ff · 1fac6 · 1fac8 · 1facd · 1face · 1facf · 1fad0 · 1fad1 · 1fad2 · 1fad3 · 1fad4 · 1fad5 · 1fad6 · 1fad7 · 1fad8 · 1fad9 · 1fada · 1fadb · 1fadc · 1fadf · 1fae0 · 1fae1 · 1fae2 · 1fae3 · 1fae4 · 1fae5 · 1fae6 · 1fae7 · 1fae8 · 1fae9 · 1faea · 1faef · 1faf0 · 1faf0-1f3fb · 1faf0-1f3fc · 1faf0-1f3fd · 1faf0-1f3fe · 1faf0-1f3ff · 1faf1 · 1faf1-1f3fb · 1faf1-1f3fb-200d-1faf2-1f3fc · 1faf1-1f3fb-200d-1faf2-1f3fd · 1faf1-1f3fb-200d-1faf2-1f3fe · 1faf1-1f3fb-200d-1faf2-1f3ff · 1faf1-1f3fc · 1faf1-1f3fc-200d-1faf2-1f3fb · 1faf1-1f3fc-200d-1faf2-1f3fd · 1faf1-1f3fc-200d-1faf2-1f3fe · 1faf1-1f3fc-200d-1faf2-1f3ff · 1faf1-1f3fd · 1faf1-1f3fd-200d-1faf2-1f3fb · 1faf1-1f3fd-200d-1faf2-1f3fc · 1faf1-1f3fd-200d-1faf2-1f3fe · 1faf1-1f3fd-200d-1faf2-1f3ff · 1faf1-1f3fe · 1faf1-1f3fe-200d-1faf2-1f3fb · 1faf1-1f3fe-200d-1faf2-1f3fc · 1faf1-1f3fe-200d-1faf2-1f3fd · 1faf1-1f3fe-200d-1faf2-1f3ff · 1faf1-1f3ff · 1faf1-1f3ff-200d-1faf2-1f3fb · 1faf1-1f3ff-200d-1faf2-1f3fc · 1faf1-1f3ff-200d-1faf2-1f3fd · 1faf1-1f3ff-200d-1faf2-1f3fe · 1faf2 · 1faf2-1f3fb · 1faf2-1f3fc · 1faf2-1f3fd · 1faf2-1f3fe · 1faf2-1f3ff · 1faf3 · 1faf3-1f3fb · 1faf3-1f3fc · 1faf3-1f3fd · 1faf3-1f3fe · 1faf3-1f3ff · 1faf4 · 1faf4-1f3fb · 1faf4-1f3fc · 1faf4-1f3fd · 1faf4-1f3fe · 1faf4-1f3ff · 1faf5 · 1faf5-1f3fb · 1faf5-1f3fc · 1faf5-1f3fd · 1faf5-1f3fe · 1faf5-1f3ff · 1faf6 · 1faf6-1f3fb · 1faf6-1f3fc · 1faf6-1f3fd · 1faf6-1f3fe · 1faf6-1f3ff · 1faf7 · 1faf7-1f3fb · 1faf7-1f3fc · 1faf7-1f3fd · 1faf7-1f3fe · 1faf7-1f3ff · 1faf8 · 1faf8-1f3fb · 1faf8-1f3fc · 1faf8-1f3fd · 1faf8-1f3fe · 1faf8-1f3ff · 203c · 2049 · 2122 · 2139 · 2194 · 2195 · 2196 · 2197 · 2198 · 2199 · 21a9 · 21aa · 23-20e3 · 231a · 231b · 2328 · 23cf · 23e9 · 23ea · 23eb · 23ec · 23ed · 23ee · 23ef · 23f0 · 23f1 · 23f2 · 23f3 · 23f8 · 23f9 · 23fa · 24c2 · 25aa · 25ab · 25b6 · 25c0 · 25fb · 25fc · 25fd · 25fe · 2600 · 2601 · 2602 · 2603 · 2604 · 260e · 2611 · 2614 · 2615 · 2618 · 261d · 261d-1f3fb · 261d-1f3fc · 261d-1f3fd · 261d-1f3fe · 261d-1f3ff · 2620 · 2622 · 2623 · 2626 · 262a · 262e · 262f · 2638 · 2639 · 263a · 2640 · 2642 · 2648 · 2649 · 264a · 264b · 264c · 264d · 264e · 264f · 2650 · 2651 · 2652 · 2653 · 265f · 2660 · 2663 · 2665 · 2666 · 2668 · 267b · 267e · 267f · 2692 · 2693 · 2694 · 2695 · 2696 · 2697 · 2699 · 269b · 269c · 26a0 · 26a1 · 26a7 · 26aa · 26ab · 26b0 · 26b1 · 26bd · 26be · 26c4 · 26c5 · 26c8 · 26ce · 26cf · 26d1 · 26d3 · 26d3-fe0f-200d-1f4a5 · 26d4 · 26e9 · 26ea · 26f0 · 26f1 · 26f2 · 26f3 · 26f4 · 26f5 · 26f7 · 26f7-1f3fb · 26f7-1f3fc · 26f7-1f3fd · 26f7-1f3fe · 26f7-1f3ff · 26f8 · 26f9 · 26f9-1f3fb · 26f9-1f3fb-200d-2640-fe0f · 26f9-1f3fb-200d-2642-fe0f · 26f9-1f3fc · 26f9-1f3fc-200d-2640-fe0f · 26f9-1f3fc-200d-2642-fe0f · 26f9-1f3fd · 26f9-1f3fd-200d-2640-fe0f · 26f9-1f3fd-200d-2642-fe0f · 26f9-1f3fe · 26f9-1f3fe-200d-2640-fe0f · 26f9-1f3fe-200d-2642-fe0f · 26f9-1f3ff · 26f9-1f3ff-200d-2640-fe0f · 26f9-1f3ff-200d-2642-fe0f · 26f9-fe0f-200d-2640-fe0f · 26f9-fe0f-200d-2642-fe0f · 26fa · 26fd · 2702 · 2705 · 2708 · 2709 · 270a · 270a-1f3fb · 270a-1f3fc · 270a-1f3fd · 270a-1f3fe · 270a-1f3ff · 270b · 270b-1f3fb · 270b-1f3fc · 270b-1f3fd · 270b-1f3fe · 270b-1f3ff · 270c · 270c-1f3fb · 270c-1f3fc · 270c-1f3fd · 270c-1f3fe · 270c-1f3ff · 270d · 270d-1f3fb · 270d-1f3fc · 270d-1f3fd · 270d-1f3fe · 270d-1f3ff · 270f · 2712 · 2714 · 2716 · 271d · 2721 · 2728 · 2733 · 2734 · 2744 · 2747 · 274c · 274e · 2753 · 2754 · 2755 · 2757 · 2763 · 2764 · 2764-fe0f-200d-1f525 · 2764-fe0f-200d-1fa79 · 2795 · 2796 · 2797 · 27a1 · 27b0 · 27bf · 2934 · 2935 · 2a-20e3 · 2b05 · 2b06 · 2b07 · 2b1b · 2b1c · 2b50 · 2b55 · 30-20e3 · 3030 · 303d · 31-20e3 · 32-20e3 · 3297 · 3299 · 33-20e3 · 34-20e3 · 35-20e3 · 36-20e3 · 37-20e3 · 38-20e3 · 39-20e3 · a9 · ae · e50a
Physics — curated scientific apparatus and components · 5 · CC0
asset:svg/physics/<name>.svg
effusion-reservoir · rocket-cutaway · rocket-first-stage · rocket-second-stage · rocket-upper-stage
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("black") | exact-black full-colour default; use mono for intentional grayscale |
| 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. Use --canvas 4:5,
--canvas square, or --canvas 16:9 at preview/record time so the file stays
unchanged. The Reactive stories
chapter shows the full workflow and the responsive manual-layout pattern.
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.
Gold pattern: let one equation evolve
For a solution Reel, do not stack five complete formulas or cut to a blank screen between steps. Declare one equation and rewrite it in place:
equation(work, (cx, 520), `x^2+2x=3`, 54);
rewrite(work, `x^2+2x+1=4`, 0.85, smooth);
wait(0.8);
rewrite(work, `(x+1)^2=4`, 0.85, smooth);
The viewer’s eye can follow the terms that move while unchanged symbols stay
put. Manic matches rendered LaTeX by reading order, mathematical layout role,
and nested math-style depth; an exponent therefore cannot become a denominator
or a different level of an exponent tower merely because both use the same
digit. When a fraction, radical, or grouping changes the structure of one
side, that side cleanly leaves and reforms while the compatible side remains.
Simple replacements such as 2 → 3 are staged as leave-then-enter, avoiding a
temporary 23; pure additions can appear immediately. You still supply and own
every correct step. For phone video, use one meaningful
rewrite per narration beat, keep the equation in the media safe region, pause
after the important result, and place any related plot or diagram motion beside
the rewrite in par.
2. Let the format own layout
canvas("9:16");
template("black"); // optional: black is the DSL default
creator(me, "@anish2good name=Proof_Daily tagline=Think_then_prove \
yt=zarigatongy x=@anish2good web=maniclang.com \
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.
The endcard deliberately uses an editorial action row rather than a fully neon-filled button: a quiet inset surface keeps the creator name and final idea dominant, while a small two-colour rail and arrow chip carry the accent palette. On a black template, reserve bright colour for those details and keep CTA copy short enough to read in one glance.
For a channel-link row without external assets, choose footer=social and set
the platform values directly: yt=zarigatongy x=@anish2good web=maniclang.com. 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");
watermark(manicMark, (w*0.895-100, h*0.075+24), "Made With Manic");
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 check examples/perfect-reel.manic --canvas all
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:
- Does
manic check your-file.manic --canvas allpass portrait, feed, square, and landscape? See Visual checks for fixes. - 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: Turn one polished scene into a reactive story →
Reference: Explore every Creator control and format variant.
Reactive stories — change the idea, not the scene
A polished Reel feels continuous: the equation changes, the point moves, the
graph responds, and the caption explains the same idea without cutting to a
blank screen. In Manic, build that as one persistent visual world and move it
forward with named step blocks.
This is the creator mental model:
- Declare the cast once — equation, plot, diagram, labels, caption, profile.
- Give each story beat a short, meaningful name.
- Inside that step, describe only what changes.
- Leave everything else alone; it remains on screen automatically.
The result is easier to write, easier to revise, and easier for a viewer’s eye to follow.
One story, three layers
Think of a reactive video as three simple layers:
| layer | Manic vocabulary | creator decision |
|---|---|---|
| World | shapes, equations, plots, text, Creator profile | What should remain recognisable throughout? |
| Story | step("question"), step("explain"), step("result") | What changes in the viewer’s understanding? |
| Motion | rewrite, to, draw, say, show, fade | How should that change become visible? |
step does not introduce a new kind of animation. It gives ordinary Manic
verbs a named conceptual boundary. Its children begin together, its duration is
the longest child, and any entity not mentioned persists.
step("measure-slope") {
rewrite(work, `f'(x)=0.70x`, 0.90, smooth);
to(tangent, x, 2.8, 3.20, smooth);
to(rate, x, 2.8, 3.20, smooth);
say(caption, "The tangent and its slope update together.", 0.40);
}
wait(0.60);
Here the formula, tangent, live value, and explanation share one beat. The curve and every other unmentioned object remain exactly where they were.
A creator-first workflow
1. Start with the promise
Before writing motion, write the story as four or five beat names:
question → measure-slope → find-the-flat-point → see-the-derivative → takeaway
Use names that describe understanding, not implementation. show-blue-line is
fragile; see-the-derivative still makes sense after the design changes.
For a short educational Reel, a reliable shape is:
- Hook: pose one visual question in the first two seconds.
- Explore: change one or two connected representations.
- Payoff: make the important relationship visible.
- Takeaway: hold one memorable sentence or result.
The same structure works for geometry proofs, physics simulations, chemistry reactions, data stories, algorithms, and product explainers. Reactive is a story pattern, not a mathematics-only feature.
2. Declare the world once
Give every continuing visual a stable id. Do not create equation1,
equation2, and equation3 when the viewer should perceive one evolving
equation. Keep work and rewrite it.
equation(work, (cx, 390), `f(x)=0.35x^2`, 51);
plot(curve, (cx, 1050), 115, 62, "0.35*x*x", (-3.5, 3.5));
text(caption, (cx, 1450), "How steep is this curve?");
Stable identity is what makes continuity possible. It also means a later style change, position adjustment, or wording fix happens in one place.
3. Change only what matters
Inside a step, pair the representations that explain one another:
step("find-the-flat-point") {
rewrite(work, `f'(0)=\textcolor{lime}{0}`, 0.85, smooth);
to(tangent, x, 0, 1.80, smooth);
to(rate, x, 0, 1.80, smooth);
show(vertex, 0.40);
say(caption, "At the vertex the tangent is flat.", 0.40);
}
Do not fade and rebuild the whole screen for every sentence. Preserve context,
move the smallest meaningful parts, and let the viewer compare before and
after. For equations, rewrite retains safely matched LaTeX pieces and brings
only the changed pieces in or out. Matching understands coarse mathematical
layout roles and nested math-style depth, so the 2 in x^2 cannot jump into
the denominator of \frac{b}{2a}, and one level of an exponential tower cannot
become another. The same rule protects logarithm bases, integral limits, and
derivative orders. Repeated symbols keep their reading order, so matrices and
formulas containing several zeros or variables do not send copies across one
another. When one equation side gains a fraction, radical, or grouping
structure, that side leaves before its replacement enters; the compatible
side and equality stay visible. For a globally incompatible change, Manic uses
a staged whole-equation dissolve with only a short, dim overlap. Ordinary glyph
replacements follow the same reading rule—the old value leaves before its
successor becomes clear—while pure additions can still enter immediately.
There is no extra mode to choose.
Manic animates the states you author; it does not solve or verify the maths. Write each intermediate LaTeX state exactly as it should appear.
For the cleanest teaching motion, keep genuinely unchanged work unchanged in the next LaTeX string. A stable right-hand side, fraction, or matrix remains recognisable while the edited side changes. Large changes are still valid—the engine will favour a clean dissolve over a misleading glyph scramble.
4. Give the result room to land
A step ends when its longest animation ends. Add a short wait after an
important step so the viewer can read the settled frame:
step("result") {
rewrite(work, `\text{slope of }f=\textcolor{magenta}{f'}`, 0.95, smooth);
pulse(curve, 0.70);
pulse(derivative, 0.70);
say(caption, "The derivative is the curve of all local slopes.", 0.40);
}
wait(1.80);
Use seq { ... } inside a step only when that conceptual beat genuinely needs
an internal order. Use par for anonymous choreography; use step when the
beat should have a name and meaning.
Creator polish that matters
- Keep one focal change per step. Several synchronized views are fine when they all explain that one change.
- Reuse semantic colours: for example, cyan for the original function, magenta for its derivative, and lime for the key result.
- Put captions in a stable safe region. Let the words change with
sayinstead of making the viewer search for a new text block. - Prefer smooth movement and local equation rewrites over full-screen fades.
- Write for the final phone size. Short captions and one- or two-line equations beat dense slides.
- Keep the Creator profile and social footer consistent across a series, but do not let branding compete with the explanatory stage.
- Preview the hook, busiest transition, result, and end hold as still frames before recording the full video.
Named steps are editing markers
Every step name is automatically exported at the step’s start in
markers.json and becomes a first-class preview/recording boundary. Inspect the
story or open one beat directly:
manic stages examples/reactive-world.manic
manic examples/reactive-world.manic --stage find-the-flat-point
Those semantic boundaries make it easier to:
- align narration, music, captions, and sound effects;
- review or seek directly to a conceptual beat;
- identify a clean hook, explanation, result, or takeaway;
- export one stage or an inclusive stage range without copying timestamps.
Names must be non-empty, unique, and top-level. If you only need a timestamp
without a reactive state change, use mark("name") instead.
Publish one story in four formats
A named-step story can now be reframed at render time without editing its
source. Use --canvas to override the logical canvas before Manic calculates
w, h, cx, cy, macros, and build-time layout branches:
manic examples/reactive-multiformat.manic --canvas portrait --record out-reel --preset reel
manic examples/reactive-multiformat.manic --canvas 4:5 --record out-feed
manic examples/reactive-multiformat.manic --canvas square --record out-square
manic examples/reactive-multiformat.manic --canvas 16:9 --record out-lesson
The story’s named steps, equation continuity, timing, and identity remain the
same. Only the logical layout changes. --canvas is separate from --preset:
the canvas controls composition; the preset controls recording quality, frame
rate, container, and engine branding.
Creator Kit regions already read the active canvas. For a hand-composed scene,
prefer w, h, cx, and cy, then use a small build-time branch when the
composition should genuinely change shape:
if h > 1.45*w {
// vertical stack for a Reel or Short
}
else if w > 1.25*h {
// explanation left, visual stage right
}
else {
// compact square / 4:5 feed composition
}
An override cannot make hard-coded coordinates responsive by itself. The author still decides the useful composition for each shape of screen; Manic ensures every branch receives the correct dimensions before construction and keeps the semantic story unchanged.
Complete examples to learn from
- Reactive world — the best compact starting point: one curve, one equation, one tangent, and five named steps.
- Reactive multi-format — one source and one named timeline, with vertical, feed, square, and landscape compositions.
- Parameter journeys — one visible value drives a plot, analysis views, geometry, and a live readout without rebuilding the world.
- Reactive integral — an equation, integrand, antiderivative, and visual verification in one continuous world.
- Reactive notation — mathematics, physics, chemistry, biology, logic, and custom LaTeX notation.
- Math journey — a playful long-form progression from Class 1 arithmetic to PhD-level notation.
- Quadratic continuity — a focused benchmark for retaining unchanged equation pieces.
Start by copying reactive-world.manic, rename its steps around your idea, and
replace one representation at a time. Keep the world; change the understanding.
Next: Navigate and export the story by stage →
Story stages — edit the idea, not the timestamp
A creator usually thinks in beats—question, intuition, experiment,
proof, takeaway—rather than 6.35 seconds. Manic uses the names already
written in step(...) as the editing and publishing structure for the movie.
There is no second timeline file to maintain.
step("question") {
show(prompt, 0.4);
say(caption, "Where does the slope become zero?", 0.35);
}
wait(0.8);
step("experiment") {
to(tangent, x, 0, 1.8, smooth);
to(rate, x, 0, 1.8, smooth);
}
wait(0.7);
step("takeaway") {
rewrite(work, `f'(0)=0`, 0.8, smooth);
}
wait(1.5);
The wait after a step belongs to that stage. This makes a stage export include
the transition and the time the viewer needs to absorb its settled result.
The next stage begins exactly where its next step begins.
See the story before opening a window
manic stages examples/reactive-world.manic
The report lists every stage’s start, end, and complete duration:
stages — examples/reactive-world.manic (5 stages, 13.55s authored)
# stage start end duration
1 question 0.00s 2.15s 2.15s
2 measure-slope 2.15s 5.95s 3.80s
3 find-the-flat-point 5.95s 8.40s 2.45s
4 see-the-derivative 8.40s 10.80s 2.40s
5 takeaway 10.80s 13.55s 2.75s
Use this before recording to catch a rushed hook, an overlong setup, or a takeaway without enough reading time.
Preview one stage
manic examples/reactive-world.manic --stage find-the-flat-point
Manic evaluates the complete persistent world at the stage’s real start, then limits playback and scrubbing to that stage. Nothing is reconstructed as a separate scene, so incoming equation, plot, diagram, and parameter state remain correct.
The live player adds a stage strip above the transport bar:
- click a stage segment to jump to it;
- use
1–9to jump to visible stages; - press
Rto restart at the selected range’s beginning; - drag the progress bar to scrub only inside the selected range;
- read the current stage and selected range in the HUD.
Record a single stage
The same flag selects the recording range:
manic examples/reactive-world.manic \
--stage see-the-derivative \
--record out-derivative \
--preset reel
For a clean editing clip without the preset’s branding intro/outro:
manic examples/reactive-world.manic \
--stage see-the-derivative \
--record out-derivative \
--preset test \
--no-brand
Export several stages
Use an inclusive named range when a clip should carry a short story arc:
manic examples/reactive-world.manic \
--from-stage measure-slope \
--to-stage takeaway \
--record out-slope-to-takeaway
--to-stage takeaway includes the whole takeaway stage. With only
--from-stage, recording continues through the authored ending. With only
--to-stage, it begins at the start of the movie.
Named ranges and numeric --from/--to are intentionally separate. Use stage
names for story editing and numeric seconds only for a deliberately precise
technical trim. Unknown names report the available stages and suggest a close
match.
Recording metadata stays useful
markers.json now includes:
- the selected source range;
- clipped stage intervals with relative
t,end, andduration; - each stage’s original
source_t; - marks and sections filtered to the exported clip and shifted to clip time.
That means narration, captions, music cues, and downstream editing tools can work from zero-based clip timing without losing the position in the source story.
Combine stages with output formats
Stage selection and canvas selection are independent:
manic examples/parameter-journeys.manic --stage takeaway --canvas portrait --record out-reel
manic examples/parameter-journeys.manic --stage takeaway --canvas square --record out-square
manic examples/parameter-journeys.manic --stage takeaway --canvas landscape --record out-lesson
The named idea stays the same while the responsive layout changes. Before the
batch render, run
manic check examples/parameter-journeys.manic --canvas all to review every
stage in every supported shape.
Choosing useful names
- Name the change in understanding:
see-the-derivative, notshow-pink-line. - Keep a short video to roughly three to six stages.
- Put several synchronized changes inside one stage only when they explain the same idea.
- Add a deliberate
waitafter the important result. - Use
mark("cue")for an editor timestamp that is not a story stage. - Use
section("Part Two")when the video itself needs a visible chapter card.
Start with Reactive world, run
manic stages examples/reactive-world.manic, and preview its
find-the-flat-point stage.
Next: Review every format before publishing →
Parameter journeys — one value, many connected views
A parameter journey shows how an idea behaves across a family of cases. Instead of rebuilding the scene for “negative”, “zero”, and “positive”, declare one visible value, connect it to the existing world once, and animate only that value.
parameter(a, (cx, 140), -1.2, -1.5, 1.5, "a", 2);
plot(curve, (cx, 720), 110, 58, "x*x", (-3.3,3.3));
circle(point, (cx, 1200), 22);
counter(magnitude, (cx, 1360), 0, 2, "a² = ", "");
bind(a, curve, formula, "0.22*p*x*x");
bind(a, point, x, w*0.18, w*0.82);
bind(a, point, scale, "0.78+0.22*abs(p)");
bind(a, magnitude, value, "p*p");
step("flatten") {
to(a, value, 0, 2.2, smooth);
}
step("opens-up") {
to(a, value, 1.25, 2.4, smooth);
}
The plot, point, scale, number, and native parameter control move on the same continuous clock. Nothing is replaced and no scene construction is duplicated.
The three-part mental model
| part | vocabulary | creator decision |
|---|---|---|
| Expose | parameter | Which value should the viewer follow? |
| Connect | bind | Which representations explain that value? |
| Travel | step + to(parameter,value,…) | Which cases tell the clearest story? |
This pattern is domain-neutral. The parameter can be a quadratic coefficient, damping factor in a formula plot, probability, sample-size readout, geometric position, opacity comparison, colour phase, or any other scalar that makes the idea easier to see.
Declare the visible parameter
parameter(id, (x,y), initial, min, max, ["label"], [decimals]);
parameter produces a typeset numeric readout and a compact track/dot widget.
The initial value must be inside a finite min..max range. Animated values are
clamped to that range, which prevents an accidental journey from leaving the
meaningful domain.
The whole widget is tagged id.widget, so it can enter with the rest of the
story:
hidden(a.widget);
show(a.widget, 0.4);
It is an authored animation control, not an interactive slider in the exported
video. Use ordinary to to move it; stateless evaluation keeps preview seeking
and recording deterministic.
Connect with a responsive range
The simplest binding maps the parameter’s declared minimum and maximum onto two output values:
bind(a, point, x, w*0.18, w*0.82);
bind(a, label, opacity, 0.25, 1);
bind(a, arrow, angle, -25, 25);
This form is especially useful for positions because the endpoints are normal
Manic expressions. w, h, cx, and cy are evaluated before the binding is
created, so the same source can give each output format an appropriate range.
Connect with a formula
Use a string formula for a nonlinear relationship:
bind(a, point, scale, "0.8+0.2*abs(p)");
bind(a, result, value, "p*p");
bind(damping, wave, formula, "exp(-p*abs(x))*cos(4*x)");
Inside a binding formula, p is the live parameter. A plot-formula binding also
provides x as the plot coordinate. The usual arithmetic, constants, and
functions—sin, cos, exp, sqrt, abs, round, and others—work here.
Bindable properties are x, y, opacity, scale, angle, hue, value,
trace, and a plot’s formula. A value target must be a counter. A formula
target must be a plot.
When a plot formula changes, its existing tangent, normal, slope, area,
integral, and moving mark views receive the new function before they recompute.
That is the important reactive promise: the analysis stays mathematically tied
to the curve the viewer sees.
Choose cases, not frames
Use named steps to describe meaningful cases:
step("underdamped") {
to(damping, value, 0.12, 2.0, smooth);
say(caption, "The oscillation survives for longer.", 0.35);
}
step("strong-damping") {
to(damping, value, 0.85, 2.0, smooth);
say(caption, "The same response now settles quickly.", 0.35);
}
The stage names should explain the cases to a human. The parameter supplies the
continuous motion between them; bind keeps every connected view synchronized.
Creator design tips
- Follow one primary parameter per short scene. Several controls are possible, but the audience should know which value is changing.
- Connect two or three complementary representations—a formula and plot, a geometry and measurement, or a simulation curve and readout.
- Give the parameter a meaningful range. Avoid travelling through singular or irrelevant values merely because the engine allows them.
- Use responsive range bindings for positions and formula bindings for meaning.
- Hold the important cases briefly. Smooth motion reveals the trend; the hold lets the viewer name it.
- Keep stable ids. Parameter journeys are an extension of the persistent-world model, not a reason to create replacement scenes.
- Run
manic check FILE.manic --canvas allafter the journey is complete.
Complete example
Parameter journeys is the polished reference:
one coefficient drives a quadratic, live tangent and slope, moving point,
changing scale, and a² readout across portrait, feed, square, and landscape.
Current bindings target 2D entity properties and formula plots. Constructors that perform expensive build-time work—such as re-simulating a full physics system or changing the number of generated objects—remain build-time choices; represent those journeys with a formula/readout today rather than implying that Manic silently recomputed the model.
Next: Meet the shapes that make up the persistent world →
Visual checks — review every format before publishing
A responsive Manic story can share its content and timing across a Reel, feed post, square video, and landscape lesson. Before recording those versions, run one publishing audit:
manic check examples/reactive-multiformat.manic --canvas all
Manic rebuilds the source at four logical canvases—portrait, 4:5 feed, square, and 16:9 landscape—then checks the settled frame of every named story stage. A clean result looks like this:
ok — examples/reactive-multiformat.manic [portrait]: visual checks passed
ok — examples/reactive-multiformat.manic [feed]: visual checks passed
ok — examples/reactive-multiformat.manic [square]: visual checks passed
ok — examples/reactive-multiformat.manic [landscape]: visual checks passed
ok — examples/reactive-multiformat.manic: visual audit passed all 4 formats
Use this after the story and layout feel right, but before spending time on the final render.
What the first audit catches
Visual-check v1 reports four common publishing problems:
| check | what it protects |
|---|---|
| Canvas bounds | text, equations, images, or primary objects outside the frame |
| Creator safe area | content underneath platform controls or too close to a protected edge |
| Content overlap | two substantial text, equation, or image boxes competing for the same space |
| Readability | text or rendered notation that is too small for the selected format |
Each message names the format, story stage, time, and responsible entity, then suggests a practical fix:
warning [square · takeaway @ 8.45s]: `caption` overlaps `result`
entities: `caption` and `result`
suggestion: separate the entities, shorten/wrap the text, or reflow this format
The command exits unsuccessfully when it finds either an error or warning, so
it can guard a publishing script or CI job. Ordinary manic check file.manic
remains the fast parse-and-validation check. To validate only one responsive
layout, keep using manic check file.manic --canvas square.
Why named steps improve the result
The audit reviews the settled state at the end of each step("name") (or
marker-defined stage), not every frame of a transition. That avoids treating a
deliberate entrance, exit, or equation rewrite crossfade as a layout failure.
It also makes a diagnosis useful: takeaway tells you which idea needs fixing,
not merely that something happened at 8.45 seconds.
If a file has no named stage, Manic checks its final authored frame. For a
creator story, prefer meaningful names such as question, experiment,
proof, and takeaway; they help editing, seeking, publishing, and review.
Fixes that stay reusable
When the audit finds a problem, preserve one responsive source:
- Position with
w,h,cx, andcyinstead of copying pixel coordinates between files. - Give continuing content stable ids so a message points to the same entity in every stage.
- Shorten copy or use
wrap(id, width)before reducing the type size. - Use one small
if h > 1.45*w { ... } else if w > 1.25*h { ... } else { ... }layout branch when the composition genuinely needs to reflow. - Choose the correct Creator
safe=shorts|reels|tiktok|cleanprofile. Usesafezone(...)while previewing the protected area; it is a design guide, not content for the finished video. - Leave enough room for the busiest settled stage, not only the opening frame.
The gold reference is
reactive-multiformat.manic. It uses one
timeline and passes the audit in all four formats.
What still needs human review
Visual-check v1 is intentionally conservative. It does not yet judge collision paths during transitions, camera-transformed world or 3D bounds, detached labels and links, reading speed, or whether an equation agrees mathematically with a plot. It also cannot decide whether the hierarchy is beautiful or the hook is compelling.
After the automated audit passes, preview the hook, busiest transition, result, and end hold at phone size. The command catches mechanical layout mistakes; the creator still owns clarity, rhythm, and taste.
Next: Animate one value through many connected views →
Motion graphics — move ideas, not layers
Motion Graphics V2 is built around continuity. Keep one object alive while it moves, follows another object, changes visual form, joins a layout, or turns as part of a system. The timeline remains deterministic and freely scrubbable.
There is no V2 mode or production flag. Write an ordinary .manic file and use
the relationship only where the story needs it.
The complete everyday vocabulary
| Creator intent | Manic | Result |
|---|---|---|
| Keep one object beside another | attach(child, target, [(dx,dy)]) | the child follows every resolved target position |
| Stop following | attach(child, none) | the child releases at the settled position without a second verb |
| Change visual identity | become(source, blueprint, [dur], [ease]) | the source keeps its id and settles exactly on the blueprint |
| Turn a whole arrangement | turn(id_or_tag, pivot, degrees, [dur], [ease]) | every member follows the same circular pivot motion |
| Move a real object on a path | travel(object, path, dur, ease) | the object arrives and remains at the endpoint |
| Send temporary or sustained path emphasis | `flow(path, dur, [forward | reverse |
| Add contained ambient life | wander(particles, dur) | seeded, repeatable motion within the container |
| Reorganize the same particles | `arrange(particles, region, “random | grid |
| Progressively move a collection | stream(particles, path, dur, spread, ease) | seeded persistent items leave in a natural order and retain arrival/speed measurements |
| Release particles with individual speeds | emit(particles, path, uniform|maxwell, dur, spread, ease) | every persistent item receives a deterministic sampled speed; faster items traverse sooner |
| Follow a visual field | advect(particles, vectorfield, dur, rate) | every persistent item follows the same bounded formula field with seekable deterministic motion |
| Route a collection through choices | branch(particles, path_tag, dur, ease) | every persistent item makes seeded choices in one authored acyclic network and retains destination/step/arrival measurements |
| Catch particles by a real measurement | collect(livehistogram, particles, speed|arrival|outcome|steps, dur, ease) | each item visibly settles in the bin selected by the same measurement used by observers |
| Show what the process measured | observe(counter_or_livehistogram, particles, arrived|speed|arrival|outcome|steps) | counters and bars stay connected to the same motion instead of being faked separately |
| Change an ordinary property | to(id, property, value, dur, ease) | the general escape hatch remains available |
The three V2 words add relationships; they do not replace move, travel,
flow, arrange, spin, transform, rewrite, or morph.
One process, several truthful views
Use stream when the authored objects themselves should leave progressively.
Use flow when only a temporary signal should pass. A process can drive more
than one observer without manually synchronizing keyframes:
rect(source, (170,300), 210,230);
particles(packets, source, 42, 5, 17);
spline(route, (275,300), (410,140), (560,470), (720,285));
livehistogram(speeds, (980,335), 0.55, 1.05, 10, 430, 220, cyan);
counter(arrivals, (640,610), 0, 0, "arrived ", " / 42");
par {
stream(packets, route, 4.2, 34, smooth);
observe(speeds, packets, speed);
observe(arrivals, packets, arrived);
}
Source order inside the par matters only for the build contract: write
stream first so the following observers can read its compiled measurements.
Playback still starts all three clips together. Seeking remains deterministic;
the collection seed reproduces the same start order, travel times, endpoints,
counter, and histogram.
speed uses normalized authored-path lengths per second rather than screen
pixels, so changing the canvas or render scale does not change the measurement.
arrival is elapsed seconds inside the process beat. A livehistogram requires
an explicit range; observations outside it clamp to the end bins instead of
silently changing the axis.
The minimal complete example is:
// PROCESS FOUNDATION — the smallest complete example.
// A real persistent collection streams along a path. Both observers read the
// compiled process measurements; neither is animated with guessed values.
title("A Collection Becomes a Process");
canvas("16:9");
template("blank");
watermark(mark, (170, 58), "Made With Manic");
text(kicker, (640, 52), "MANIC · DETERMINISTIC PROCESS");
text(headline, (640, 100), "One journey. Two truthful views.");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
rect(source, (170, 300), 210, 230);
outlined(source); outline(source, dim); stroke(source, 3);
text(sourceLabel, (170, 440), "persistent collection");
size(sourceLabel, 18); color(sourceLabel, dim);
particles(packets, source, 42, 5, 17);
spline(route, (275, 300), (410, 140), (560, 470), (720, 285));
stroke(route, 4); color(route, fg); untraced(route);
livehistogram(speeds, (980, 335), 0.55, 1.05, 10, 430, 220, cyan);
text(speedLabel, (980, 190), "normalized speed");
size(speedLabel, 20); bold(speedLabel);
counter(arrivals, (640, 610), 0, 0, "arrived ", " / 42");
size(arrivals, 25); color(arrivals, dim);
text(caption, (640, 665), "stream moves real objects · observe reads the same process");
size(caption, 20); color(caption, dim);
hidden(packets); hidden(speeds); hidden(arrivals); hidden(caption);
step("introduce") {
par {
show(packets, 0.45);
draw(route, 0.65);
show(speeds, 0.45);
show(arrivals, 0.35);
show(caption, 0.35);
}
}
wait(0.35);
step("stream-and-observe") {
par {
stream(packets, route, 4.2, 34, smooth);
observe(speeds, packets, speed);
observe(arrivals, packets, arrived);
}
}
wait(1.0);
Branching networks: author paths, keep the evidence
branch is the reusable one-of-many process. Tag ordinary paths into one
network; the first and last points establish each edge’s direction:
line(entry, (100,260), (250,260)); tag(entry, routes);
line(a, (250,260), (430,120)); tag(a, routes);
line(b, (250,260), (430,260)); tag(b, routes);
line(c, (250,260), (430,400)); tag(c, routes);
livehistogram(destinations, (680,300), 0, 3, 3, 300, 240, cyan);
par {
branch(requests, routes, 4, smooth);
observe(destinations, requests, outcome);
}
The network must be connected and acyclic, with one root and at least two
terminals. Paths may converge before splitting again. Each item makes the same
seeded uniform choices in preview, recording, and direct frame seeking.
Terminals are numbered left-to-right, then top-to-bottom; outcome is that
number and steps is the number of edges traversed.
This is intentionally subject-neutral. The minimal example dispatches requests to workers; the polished example uses the same word for repeated left/right choices on a Galton board:
// GENERIC PROCESS BRANCHING — one source, three destinations.
//
// The paths carry no service semantics. `branch` only sees a directed acyclic
// network and makes one deterministic uniform choice at every fork. The same
// foundation drives the Galton-board example.
title("Process Branching — One Source, Three Destinations");
canvas("16:9");
template("blank");
watermark(mark, (145, 70), "Made With Manic");
text(head, (cx, 72), "One collection · many truthful routes");
text(sub, (cx, 112),
"Every request keeps its identity, destination, step count, and arrival time.");
size(head, 32); bold(head);
size(sub, 19); color(sub, dim);
circle(source, (180, 340), 24);
color(source, panel); outline(source, cyan); stroke(source, 3);
particles(requests, source, 54, 4, 41);
color(requests, cyan); glow(requests, 0.7); z(requests, 8);
line(entry, (205, 340), (420, 340)); tag(entry, dispatchRoutes);
spline(upper, (420, 340), (530, 190), (680, 185)); tag(upper, dispatchRoutes);
line(middle, (420, 340), (680, 340)); tag(middle, dispatchRoutes);
spline(lower, (420, 340), (530, 490), (680, 495)); tag(lower, dispatchRoutes);
color(dispatchRoutes, dim); stroke(dispatchRoutes, 3); untraced(dispatchRoutes);
rect(worker0, (735, 185), 150, 82);
rect(worker1, (735, 340), 150, 82);
rect(worker2, (735, 495), 150, 82);
for i in 0..3 {
color(worker{i}, panel); outline(worker{i}, cyan); stroke(worker{i}, 2);
}
text(w0, (735, 185), "worker 0");
text(w1, (735, 340), "worker 1");
text(w2, (735, 495), "worker 2");
size(w0, 18); size(w1, 18); size(w2, 18);
livehistogram(destinations, (1030, 350), 0, 3, 3, 330, 300, magenta);
text(histTitle, (1030, 170), "DESTINATION OUTCOME");
size(histTitle, 19); bold(histTitle); color(histTitle, dim);
counter(arrived, (1030, 555), 0, 0, "arrived ", " / 54");
size(arrived, 21); color(arrived, dim);
text(caption, (cx, 650),
"The diagram and histogram are two views of the same seeded dispatch.");
size(caption, 21); color(caption, dim);
hidden(requests); hidden(destinations); hidden(arrived); hidden(caption);
step("network") {
par {
draw(dispatchRoutes, 0.75);
show(requests, 0.35);
show(destinations, 0.45);
show(arrived, 0.35);
show(caption, 0.35);
}
}
wait(0.35);
step("dispatch") {
par {
branch(requests, dispatchRoutes, 5.0, smooth);
observe(destinations, requests, outcome);
observe(arrived, requests, arrived);
flow(dispatchRoutes, 5.0, forward, continuous);
}
}
wait(1.0);
// GALTON BOARD — RANDOM LOCALLY, PREDICTABLE GLOBALLY
//
// This is a probability story built from generic process vocabulary. Ordinary
// tagged lines form the board; `branch` preserves each ball through eight
// choices; `collect` and `observe` build the distribution from real arrivals.
title("How Random Choices Become a Bell Curve");
canvas("9:16");
template("blank");
watermark(mark,(w*0.16,h*0.042),"Made With Manic");
text(kicker,(cx,h*0.070),"PROBABILITY · RANDOM LOCALLY, ORDERED GLOBALLY");
text(headline,(cx,h*0.115),"Can random choices create a predictable shape?");
text(chapter,(cx,h*0.195),"1 · BEGIN WITH ONE FORK");
text(caption,(cx,h*0.855),"One ball can land almost anywhere.");
text(insight,(cx,h*0.815),"MORE ROUTES LEAD TO THE CENTER");
text(cta,(cx,h*0.930),"MAKE PROBABILITY VISIBLE → 8gwifi.org/manic");
size(kicker,18); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,31); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(chapter,19); color(chapter,cyan); bold(chapter); hidden(chapter);
size(caption,21); color(caption,dim); wrap(caption,w*0.84); hidden(caption);
size(insight,19); color(insight,lime); bold(insight); hidden(insight);
size(cta,21); color(cta,cyan); bold(cta); hidden(cta);
equation(law,(cx,h*0.158),`X\sim\operatorname{Binomial}\!\left(8,\frac12\right)`,29);
hidden(law);
let levels = 8;
let boardX = cx;
let topY = h*0.235;
let dx = w*0.065;
let dy = h*0.034;
// Every directed edge joins one row to the next. Converging endpoints create
// the ordinary rooted DAG followed by `branch`.
for r in 0..levels {
for k in 0..r+1 {
let x1 = boardX + (k-r*0.5)*dx;
let y1 = topY + r*dy;
let xl = boardX + (k-(r+1)*0.5)*dx;
let xr = boardX + (k+1-(r+1)*0.5)*dx;
let y2 = topY + (r+1)*dy;
line(left{r}_{k},(x1,y1),(xl,y2));
line(right{r}_{k},(x1,y1),(xr,y2));
tag(left{r}_{k},boardRoutes); tag(right{r}_{k},boardRoutes);
color(left{r}_{k},dim); color(right{r}_{k},dim);
stroke(left{r}_{k},1.7); stroke(right{r}_{k},1.7);
opacity(left{r}_{k},0.30); opacity(right{r}_{k},0.30);
dot(peg{r}_{k},(x1,y1),4.8);
color(peg{r}_{k},fg); glow(peg{r}_{k},0.36); tag(peg{r}_{k},pegs);
}
}
untraced(boardRoutes);
for k in 0..levels+1 {
let tx = boardX + (k-levels*0.5)*dx;
let ty = topY + levels*dy;
dot(exit{k},(tx,ty),5);
color(exit{k},gold); glow(exit{k},0.45); tag(exit{k},exits);
counter(bin{k},(tx,ty+28),k,0);
size(bin{k},15); color(bin{k},dim); tag(bin{k},exitLabels);
}
text(leftChoice,(boardX-dx*0.72,topY+dy*0.72),"LEFT");
text(rightChoice,(boardX+dx*0.72,topY+dy*0.72),"RIGHT");
size(leftChoice,15); size(rightChoice,15);
color(leftChoice,cyan); color(rightChoice,magenta);
hidden(leftChoice); hidden(rightChoice);
circle(source,(boardX,topY),13);
opacity(source,0);
particles(balls,source,180,4.0,73);
color(balls,cyan); glow(balls,0.78); z(balls,9);
let histY = h*0.695;
livehistogram(outcomes,(cx,histY),0,9,9,w*0.76,h*0.155,magenta);
text(histTitle,(cx,h*0.595),"WHERE 180 BALLS ACTUALLY LANDED");
size(histTitle,18); bold(histTitle); color(histTitle,dim);
counter(landed,(cx,h*0.785),0,0,"landed "," / 180");
size(landed,21); color(landed,dim);
hidden(pegs); hidden(exits); hidden(exitLabels);
hidden(balls); hidden(outcomes); hidden(histTitle); hidden(landed);
step("introduce one uncertain choice") {
par {
show(kicker,0.30);
show(headline,0.45);
show(law,0.45);
show(chapter,0.35);
show(caption,0.40);
draw(boardRoutes,0.90);
show(pegs,0.55);
show(exits,0.45);
show(exitLabels,0.45);
show(leftChoice,0.35);
show(rightChoice,0.35);
}
}
wait(0.60);
step("repeat the choice eight times") {
par {
show(outcomes,0.50);
show(histTitle,0.35);
show(landed,0.35);
say(chapter,"2 · REPEAT LEFT OR RIGHT EIGHT TIMES",0.38);
say(caption,"At every peg, each ball makes another equally likely left-or-right choice.",0.44);
}
}
wait(0.55);
step("let the crowd reveal the pattern") {
par {
branch(balls,boardRoutes,8.20,smooth);
collect(outcomes,balls,outcome,0.34,smooth);
observe(outcomes,balls,outcome);
observe(landed,balls,arrived);
show(balls,0.15);
seq {
say(chapter,"3 · WATCH 180 INDIVIDUAL JOURNEYS",0.38);
say(caption,"One route is unpredictable. The crowd begins to expose a stable pattern.",0.44);
wait(3.40);
say(caption,"Every bar is measured from the same balls you see falling—not animated separately.",0.44);
}
}
}
wait(0.65);
step("explain why the center wins") {
par {
pulse(outcomes.bars,0.80);
show(insight,0.45);
say(chapter,"4 · ORDER EMERGES FROM MANY CHOICES",0.38);
say(caption,"Extreme bins need nearly all-left or all-right. Many more mixed sequences end near the center.",0.48);
}
}
wait(0.75);
step("create with Manic") {
par {
pulse(outcomes.bars,0.75);
show(cta,0.45);
say(caption,"Describe the choices once. Manic keeps every route, arrival, count, and live distribution connected.",0.45);
}
}
wait(1.45);
The first scientific acceptance story uses exactly the same generic words for molecular motion:
// MAXWELL–BOLTZMANN PROCESS FOUNDATION
// First acceptance story for deterministic evolving collections. The emitted
// particles, arrival count, and speed histogram all come from one compiled
// process. The formula is authored truth; Manic is not pretending to be a CAS.
title("Maxwell–Boltzmann — From Motion to Distribution");
canvas("9:16");
template("blank");
watermark(mark, (155, 92), "Made With Manic");
text(kicker, (540, 80), "STATISTICAL MECHANICS · PROCESS VIEW");
text(headline, (540, 145), "How does molecular motion become a distribution?");
size(kicker, 19); color(kicker, dim); bold(kicker);
size(headline, 33); bold(headline); wrap(headline, 850);
equation(law, (540, 245),
`f(v)=4\pi\left(\frac{m}{2\pi k_BT}\right)^{3/2}v^2e^{-mv^2/(2k_BT)}`, 36);
rect(reservoir, (410, 500), 560, 330);
outlined(reservoir); outline(reservoir, dim); stroke(reservoir, 4);
line(divider, (630, 340), (630, 660));
color(divider, dim); stroke(divider, 3);
text(gasLabel, (330, 685), "seeded molecules");
size(gasLabel, 20); color(gasLabel, dim);
particles(molecules, reservoir, 72, 5, 29);
// The route starts at the aperture and bends into the measurement region.
spline(nozzle, (630, 500), (735, 430), (850, 570), (900, 735));
stroke(nozzle, 4); color(nozzle, fg); untraced(nozzle);
text(nozzleLabel, (850, 395), "effusive path");
size(nozzleLabel, 19); color(nozzleLabel, dim);
livehistogram(speedBins, (540, 1160), 0.35, 0.95, 14, 760, 300, cyan);
text(histTitle, (540, 970), "EMPIRICAL SPEEDS");
size(histTitle, 23); bold(histTitle);
text(axisUnit, (540, 1355), "normalized path lengths per second");
size(axisUnit, 18); color(axisUnit, dim);
counter(arrived, (540, 1460), 0, 0, "detected molecules ", " / 72");
size(arrived, 24); color(arrived, dim);
text(caption, (540, 1545),
"Each bar grows only when a molecule from the same process arrives.");
size(caption, 22); color(caption, dim); wrap(caption, 820);
hidden(molecules); hidden(speedBins); hidden(arrived); hidden(caption);
hidden(nozzleLabel);
step("reservoir") {
par {
show(molecules, 0.55);
show(speedBins, 0.45);
show(arrived, 0.35);
show(caption, 0.35);
}
}
wait(0.45);
step("open-the-aperture") {
par {
draw(nozzle, 0.55);
show(nozzleLabel, 0.35);
}
}
wait(0.25);
step("motion-becomes-data") {
par {
stream(molecules, nozzle, 5.2, 42, smooth);
observe(speedBins, molecules, speed);
observe(arrived, molecules, arrived);
say(caption,
"The motion and the distribution are two views of one deterministic experiment.",
0.45);
}
}
wait(0.7);
step("takeaway") {
par {
pulse(speedBins.bars, 0.8);
say(caption,
"Manic can now connect a changing collection to measurements without frame callbacks.",
0.45);
}
}
wait(1.2);
V2: emission becomes evidence
stream is ideal when a collection should share one authored journey. Use
emit when individual sampled speeds are the explanation. Add collect when
the particles themselves should settle into the measured bins:
particles(molecules, source, 180, 3.2, 73);
livehistogram(empirical, (730,560), 0, 2.2, 14, 540, 240, cyan);
par {
emit(molecules, flight, maxwell, 7.0, 48, smooth);
collect(empirical, molecules, speed, 0.48, smooth);
observe(empirical, molecules, speed);
observe(arrivals, molecules, arrived);
}
The calls are written in dependency order because each later call reads the
compiled process produced before it. They still play simultaneously inside the
par. maxwell is a normalized speed profile, not a thermodynamics solver:
the creator supplies temperature, units, and explanatory notation. Manic
truthfully samples the distribution, preserves particle identity, makes faster
samples travel sooner, catches each item in its matching bin, and reproduces
the same result from the collection seed.
Decorative fidelity remains separate from process truth. The acceptance story uses the bundled native-vector reservoir asset for the apparatus while its particles, theoretical curve, empirical bars, and counters remain ordinary Manic entities:
// EXPRESSIVE PROCESS V2 — MAXWELL–BOLTZMANN EFFUSION
//
// A polished 2.5D acceptance story inspired by engine-test-6.mov. The
// apparatus is a bundled native SVG, while every molecule, detector bin,
// arrival count, and empirical distribution remains a normal Manic entity.
// `emit`, `collect`, and `observe` all read one deterministic process.
title("Maxwell–Boltzmann Effusion — Motion Becomes Evidence");
canvas("9:16");
template("blank");
watermark(mark, (150, 90), "Made With Manic");
text(kicker, (540, 84), "STATISTICAL MECHANICS · EXPRESSIVE PROCESS V2");
text(headline, (540, 142), "A distribution you can watch forming");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
equation(law, (540, 242),
`f(v)=4\pi\left(\frac{m}{2\pi k_BT}\right)^{3/2}v^2e^{-mv^2/(2k_BT)}`, 34);
// The decorative shell is an asset; the changing evidence stays native.
svg(apparatus, (240, 540), "asset:svg/physics/effusion-reservoir.svg", 330);
text(reservoirLabel, (225, 705), "THERMAL RESERVOIR");
size(reservoirLabel, 17); bold(reservoirLabel); color(reservoirLabel, gold);
rect(detectorPanel, (730, 560), 570, 300);
filled(detectorPanel); color(detectorPanel, panel); opacity(detectorPanel, 0.92);
outlined(detectorPanel); outline(detectorPanel, dim); stroke(detectorPanel, 3);
text(detectorTitle, (730, 382), "PARTICLE SPEED v");
size(detectorTitle, 18); bold(detectorTitle); color(detectorTitle, dim);
// The invisible source is precisely at the SVG nozzle.
circle(source, (348, 540), 20);
opacity(source, 0);
particles(molecules, source, 180, 3.2, 73);
color(molecules, magenta); glow(molecules, 0.8); z(molecules, 8);
// Individual lanes diverge around this authored route before being collected.
spline(flight, (350, 540), (415, 500), (465, 515), (500, 545));
hidden(flight);
livehistogram(empirical, (730, 560), 0, 2.2, 14, 540, 240, cyan);
z(empirical, 3); hidden(empirical.count);
// Same horizontal range as the live histogram: 0 .. 4*scale = 2.2.
distribution(theory, (730, 560), "maxwell", 0.55, 0, gold);
hidden(theory.axis); hidden(theory.name);
untraced(theory.curve); z(theory, 7);
text(empiricalKey, (610, 750), "● empirical arrivals");
text(theoryKey, (850, 750), "— theoretical curve");
size(empiricalKey, 17); color(empiricalKey, cyan);
size(theoryKey, 17); color(theoryKey, gold);
counter(arrivals, (540, 850), 0, 0, "detected molecules ", " / 180");
size(arrivals, 24); color(arrivals, dim);
text(caption, (540, 920),
"A thermal source emits many molecular speeds—not one average speed.");
size(caption, 23); wrap(caption, 880); color(caption, dim);
rect(evidencePanel, (540, 1110), 880, 230);
filled(evidencePanel); color(evidencePanel, panel); opacity(evidencePanel, 0.95);
outlined(evidencePanel); outline(evidencePanel, dim); stroke(evidencePanel, 2);
text(evidenceTitle, (540, 1030), "ONE PROCESS · THREE VIEWS");
size(evidenceTitle, 18); bold(evidenceTitle); color(evidenceTitle, dim);
equation(processLaw, (540, 1100),
`\text{motion}\;\longrightarrow\;\text{arrival bin}\;\longrightarrow\;\hat f(v)`, 32);
text(truth, (540, 1190),
"The dots, bars, and count are derived from the same seeded experiment.");
size(truth, 20); color(truth, dim); wrap(truth, 790);
hidden(apparatus); hidden(reservoirLabel); hidden(detectorPanel);
hidden(detectorTitle); hidden(molecules); hidden(empirical);
hidden(empiricalKey); hidden(theoryKey); hidden(arrivals);
hidden(caption); hidden(evidencePanel); hidden(evidenceTitle);
hidden(processLaw); hidden(truth);
step("build-the-apparatus") {
par {
show(apparatus, 0.65);
show(reservoirLabel, 0.45);
show(detectorPanel, 0.55);
show(detectorTitle, 0.35);
show(empirical.axis, 0.55);
show(empirical.bars, 0.55);
show(empirical.min, 0.35);
show(empirical.max, 0.35);
show(empiricalKey, 0.35);
show(theoryKey, 0.35);
show(arrivals, 0.35);
show(caption, 0.40);
}
}
wait(0.45);
step("the-prediction") {
par {
draw(theory.curve, 0.9, smooth);
say(caption,
"Maxwell predicts a characteristic spread with a most probable speed.",
0.40);
par {
cam((cx, cy), 0.75, smooth);
zoom(1.025, 0.75, smooth);
}
}
}
wait(0.35);
step("run-the-experiment") {
par {
emit(molecules, flight, maxwell, 7.0, 48, smooth);
collect(empirical, molecules, speed, 0.48, smooth);
observe(empirical, molecules, speed);
observe(arrivals, molecules, arrived);
show(molecules, 0.15);
say(caption,
"Each molecule keeps its sampled speed. Faster molecules cross the apparatus sooner.",
0.45);
}
}
wait(0.55);
step("motion-becomes-evidence") {
par {
pulse(empirical.bars, 0.8);
show(evidencePanel, 0.40);
show(evidenceTitle, 0.35);
show(processLaw, 0.45);
show(truth, 0.40);
say(caption,
"Collection does not fake the chart: every arrival lands in the bin selected by its speed.",
0.45);
par {
cam((cx, cy), 0.65, smooth);
zoom(1.0, 0.65, smooth);
}
}
}
wait(1.4);
Parameterized fields: change the rule, then release the tracers
A field is ordinary visible Manic geometry. Keep a familiar named field, or replace its components with formulas in mathematical coordinates:
vectorfield(flow, (640,365), 375,190, uniform, 17);
parameter(mix, (640,610), 0, 0, 1, "vortex mix", 2);
bind(mix, flow, formula, "(1-p)*1.15-p*y", "p*x");
step("wind-becomes-vortex") {
to(mix, value, 1, 2.5, smooth);
}
step("release-tracers") {
advect(tracers, flow, 4.4, 0.78);
}
Here x and y are field coordinates and p is the parameter. Every arrow
updates from the same formula. advect then compiles each persistent tracer’s
journey through the settled field, so preview, recording, and direct seeking
agree. The optional rate controls distance travelled without changing the beat
duration. Reaching the field boundary safely stops a tracer.
This first slice intentionally does not infer fluid mechanics, collision behavior, obstacles, or domain meaning. It gives creators one reusable visual relationship.
// PARAMETERIZED VECTOR FIELD — one value changes the whole field.
// The same settled field then moves a persistent tracer collection. No frame
// callbacks and no manually authored particle paths are involved.
title("From Wind to Vortex");
canvas("16:9");
template("mono");
watermark(mark, (170, 55), "Made With Manic");
text(kicker, (640, 48), "MANIC · PARAMETERIZED FIELD");
text(headline, (640, 94), "One parameter reshapes every arrow");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
rect(chamber, (640, 365), 780, 410);
outlined(chamber); outline(chamber, dim); stroke(chamber, 3);
// The initial named field supplies a clear first state. `bind` then gives it
// formula components where x,y are field coordinates and p is the parameter.
vectorfield(flow, (640, 365), 375, 190, uniform, 17);
parameter(mix, (640, 610), 0, 0, 1, "vortex mix", 2);
bind(mix, flow, formula, "(1-p)*1.15-p*y", "p*x");
circle(seed, (640, 365), 128);
outlined(seed); outline(seed, dim); dashed(seed, 10, 9);
particles(tracers, seed, 32, 5, 31, "random");
color(tracers, fg); glow(tracers, 0.55);
text(caption, (640, 678), "p = 0: steady wind");
size(caption, 20); color(caption, dim);
untraced(flow); hidden(seed); hidden(tracers);
step("reveal-the-field") {
par {
draw(flow, 0.9);
show(seed, 0.4);
show(tracers, 0.55);
}
}
wait(0.5);
step("wind-becomes-vortex") {
par {
to(mix, value, 1, 2.5, smooth);
say(caption, "p = 1: the same field turns into a vortex", 0.4);
}
}
wait(0.55);
step("release-tracers") {
par {
advect(tracers, flow, 4.4, 0.78);
say(caption, "The dots follow the settled field — their paths were never drawn.", 0.4);
}
}
wait(1.2);
The same vocabulary can tell an optimization story without teaching the engine what “gradient descent” means:
// GRADIENT DESCENT AS A LEARNING STORY
// Twelve guesses begin without the answer. Every arrow supplies one local
// correction, and the same authored field carries all of them to one minimum.
title("How a Wrong Guess Learns");
canvas("9:16");
template("blank");
watermark(mark,(w*0.16,h*0.045),"Made With Manic");
text(kicker,(cx,h*0.075),"GRADIENT DESCENT · LEARNING AS MOTION");
text(headline,(cx,h*0.125),"How does a wrong guess find the minimum?");
text(chapter,(cx,h*0.265),"1 · START ANYWHERE");
text(caption,(cx,h*0.78),"Twelve guesses begin with no map of the whole landscape.");
text(result,(cx,h*0.715),"12 STARTING POINTS → 1 SHARED MINIMUM");
text(cta,(cx,h*0.91),"MAKE LEARNING VISIBLE → 8gwifi.org/manic");
size(kicker,18); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,32); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(chapter,19); color(chapter,cyan); bold(chapter); hidden(chapter);
size(caption,21); color(caption,dim); wrap(caption,w*0.84); hidden(caption);
size(result,19); color(result,lime); bold(result); hidden(result);
size(cta,21); color(cta,cyan); bold(cta); hidden(cta);
equation(lossRule,(cx,h*0.205),`L(x,y)=\frac{1}{2}x^2+y^2`);
size(lossRule,34); color(lossRule,fg); hidden(lossRule);
let fieldY = h*0.49;
rect(domain,(cx,fieldY),w*0.82,h*0.40);
outlined(domain); outline(domain,dim); stroke(domain,3);
hidden(domain);
// -grad L = (-x,-2y). The gentler vertical coefficient keeps the motion
// readable while preserving the elliptical convergence of the loss bowl.
vectorfield(descent,(cx,fieldY),w*0.37,h*0.175,"-x","-1.6*y",17);
untraced(descent);
circle(starts,(cx,fieldY),w*0.255);
outlined(starts); outline(starts,dim); dashed(starts,12,10);
hidden(starts);
particles(guesses,starts,12,8,73,"ring");
color(guesses,magenta); glow(guesses,0.78);
hidden(guesses);
circle(minimum,(cx,fieldY),14);
filled(minimum); color(minimum,lime); glow(minimum,1.0);
hidden(minimum);
circle(targetRing,(cx,fieldY),44);
outlined(targetRing); outline(targetRing,lime); stroke(targetRing,3);
hidden(targetRing);
text(minLabel,(cx,fieldY+74),"minimum");
size(minLabel,19); color(minLabel,lime); bold(minLabel); hidden(minLabel);
step("begin with imperfect guesses") {
par {
show(kicker,0.30);
show(headline,0.45);
show(lossRule,0.45);
show(chapter,0.35);
show(caption,0.40);
show(domain,0.45);
show(starts,0.45);
show(guesses,0.60);
}
}
wait(0.65);
step("reveal one local clue everywhere") {
par {
draw(descent,1.10);
fade(starts,0.45);
say(chapter,"2 · READ THE LOCAL SLOPE",0.35);
say(caption,"Every arrow gives one instruction: move opposite the local gradient.",0.42);
}
}
wait(0.55);
step("improve one small step at a time") {
par {
advect(guesses,descent,6.80,0.32);
seq {
say(chapter,"3 · FOLLOW THE CORRECTIONS",0.35);
say(caption,"Far away, the correction is large. Near the bottom, each adjustment becomes smaller.",0.42);
wait(3.05);
say(caption,"Different starting points take different journeys—but obey the same rule.",0.42);
}
}
}
wait(0.55);
step("discover the shared destination") {
par {
show(minimum,0.35);
show(targetRing,0.40);
show(minLabel,0.35);
show(result,0.45);
say(chapter,"4 · ARRIVE WITHOUT TELEPORTING",0.35);
say(caption,"Gradient descent is not a jump to the answer. It is repeated, measured improvement.",0.42);
seq {
pulse(targetRing,0.70);
pulse(guesses,0.70);
}
}
}
wait(0.75);
step("create with Manic") {
par {
pulse(minimum,0.75);
show(cta,0.45);
say(caption,"Describe the rule once. Manic keeps every guess, arrow, correction, and learning step connected.",0.45);
}
}
wait(1.45);
Manic does not infer the subject
An icon, label, or filename never changes motion semantics. Manic does not know that an object represents a load balancer, queue, topic, photon, vehicle, blood cell, or decorative spark. The creator supplies that meaning through ordinary composition:
// One selected route
par { travel(packet, lane2, 1.2, smooth); flow(lane2, 1.2); }
// Three authored deliveries together
par {
travel(copy1, lane1, 1.0, smooth);
travel(copy2, lane2, 1.0, smooth);
travel(copy3, lane3, 1.0, smooth);
}
// A path connected to nothing—motion used purely as design
flow(ribbon, 4.0, both, continuous);
seq creates an authored order. par creates simultaneous motion. travel
moves any ordinary 2-D entity and preserves its identity. flow requires only
a path; it does not require endpoints, architecture metadata, or an object to
carry. Tags let one flow address several paths when the creator wants them to
act together.
attach — author the relationship
dot(marker, (180,620), 8);
text(readout, (180,580), "sample A");
plot(curve, (180,620), 90,140,"1-exp(-x)",(0,4));
attach(readout, marker, (0,-40));
travel(marker, curve, 2, smooth);
attach(readout, none); // release at the settled endpoint
The child follows after normal tracks, reactive bindings, derived geometry, links, particle layouts, and path travel have resolved. Its opacity is multiplied by the target opacity, so a label naturally disappears with the thing it explains.
Use an offset to keep the child readable. Release at the end of a movement,
then move, fade, or reuse the child normally. Attachment cycles are rejected
while building the movie rather than failing during rendering.
become — preserve identity across a visual change
Declare the destination like any other entity and hide it when it is only a blueprint:
circle(seed, (220,700), 16); color(seed, cyan);
circle(node, (820,700), 62); color(node, magenta);
outlined(node); stroke(node, 7); hidden(node);
become(seed, node, 0.9, smooth);
Compatible geometry interpolates continuously, including circle, rectangle, line, arrow, curve, coil, arc, and equal-topology polygon/polyline pairs. Unsupported pairs use a local fade/swap/fade instead of producing broken geometry. Both paths settle on the exact target geometry and styling while the source id remains alive. A hidden blueprint does not make the transformed source disappear, and the blueprint’s own visibility is never changed.
Use rewrite for equations because it understands matching LaTeX visual parts.
After the mathematical transformation has settled, disintegrate(eq, 1.2) can
release the rendered equation as deterministic dust. It samples the actual
alpha silhouette; it is not a rectangular confetti overlay. The same treatment
also works on images and vector shapes, so it remains motion-graphics
vocabulary rather than a math-only effect.
equation(work,(cx,cy),`\int_0^1 3x^2\,dx`,64);
rewrite(work,`\left[x^3\right]_0^1=1`,1.0,smooth);
disintegrate(work,1.3);
Use morph plus to(..., morph, ...) when you explicitly need a fraction-driven
point morph or winding angle.
turn — rotate the system, not every member
circle(orbit, (540,700), 220); hidden(orbit);
particles(dots, orbit, 16, 7, 42, "ring");
turn(dots, orbit, 24, 0.65, out);
The first argument may be one entity or a tag. The pivot may be a point or an
entity. Positions follow circular paths, path endpoints and curve controls turn
with their paths, and ordinary shapes retain their group-local orientation.
Because the target is resolved from the latest authored state, turn composes
after move, travel, arrange, or an earlier turn without snapping back.
Use spin for one object’s in-place rotation. Use transform when the actual
idea is a matrix, shear, reflection, or other precise linear map.
travel versus flow
par {
draw(curve, 2, out);
travel(marker, curve, 2, out); // the marker moves and stays
flow(curve, 1); // temporary travelling emphasis
}
Use travel for a vehicle, probe, token, particle, or graph marker. Use flow
for energy, attention, traffic, or a signal that should disappear after passing.
The default remains one forward pulse. For sustained activity use
flow(curve, 4, forward, continuous); Manic chooses length-aware complete
cycles, so the stream begins empty and drains cleanly at the end. Use
flow(curve, 1, reverse, once) for generic reverse motion and both for two
independent opposing streams.
Motion-flow foundation example
This example proves the same vocabulary across four different intentions: one selected path, three motions authored in order, three motions authored together, and a free spline used only as visual design.
// motion-flow-foundation.manic — objects, paths, timing, and composition
title("Motion Flow Foundation — The Creator Decides");
canvas("9:16");
template("mono");
let u = (w+h-abs(w-h))/2160;
let left = w*0.17;
let right = w*0.83;
let upper = h*0.36;
let middle = h*0.48;
let lower = h*0.60;
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC · DOMAIN-NEUTRAL MOTION");
text(headline, (cx, h*0.13), "The path has no meaning until you give it one.");
text(mode, (cx, h*0.20), "ONE OBJECT · ONE SELECTED PATH");
text(caption, (cx, h*0.82), "Three relationships are possible. The creator activates only the middle path.");
size(kicker, 21*u); color(kicker, dim); bold(kicker);
size(headline, 36*u); bold(headline); wrap(headline, w*0.82);
size(mode, 25*u); color(mode, cyan); bold(mode);
size(caption, 24*u); color(caption, dim); wrap(caption, w*0.80);
rect(stage, (cx, h*0.49), w*0.88, h*0.56);
color(stage, panel); outline(stage, dim); opacity(stage, 0.72);
// Scene 1: one authored object chooses one authored route.
line(cold1a, (left,middle), (right,upper)); dashed(cold1a); color(cold1a, dim); stroke(cold1a, 3*u);
line(cold1b, (left,middle), (right,middle)); dashed(cold1b); color(cold1b, dim); stroke(cold1b, 3*u);
line(cold1c, (left,middle), (right,lower)); dashed(cold1c); color(cold1c, dim); stroke(cold1c, 3*u);
line(hot1b, (left,middle), (right,middle)); color(hot1b, cyan); stroke(hot1b, 7*u); untraced(hot1b);
circle(choiceA, (right,upper), 32*u); outlined(choiceA); color(choiceA, dim); stroke(choiceA, 3*u);
circle(choiceB, (right,middle), 32*u); outlined(choiceB); color(choiceB, cyan); stroke(choiceB, 4*u);
circle(choiceC, (right,lower), 32*u); outlined(choiceC); color(choiceC, dim); stroke(choiceC, 3*u);
rect(parcel, (left,middle), 34*u,34*u); color(parcel, gold); glow(parcel, 1.5);
tag(cold1a, scene1); tag(cold1b, scene1); tag(cold1c, scene1); tag(hot1b, scene1);
tag(choiceA, scene1); tag(choiceB, scene1); tag(choiceC, scene1); tag(parcel, scene1);
// Scene 2: the creator makes the same three routes active in sequence.
line(cold2a, (left,middle), (right,upper)); dashed(cold2a); color(cold2a, dim); stroke(cold2a, 3*u); hidden(cold2a);
line(cold2b, (left,middle), (right,middle)); dashed(cold2b); color(cold2b, dim); stroke(cold2b, 3*u); hidden(cold2b);
line(cold2c, (left,middle), (right,lower)); dashed(cold2c); color(cold2c, dim); stroke(cold2c, 3*u); hidden(cold2c);
line(hot2a, (left,middle), (right,upper)); color(hot2a, cyan); stroke(hot2a, 7*u); untraced(hot2a);
line(hot2b, (left,middle), (right,middle)); color(hot2b, cyan); stroke(hot2b, 7*u); untraced(hot2b);
line(hot2c, (left,middle), (right,lower)); color(hot2c, cyan); stroke(hot2c, 7*u); untraced(hot2c);
circle(roundA, (right,upper), 32*u); outlined(roundA); color(roundA, dim); stroke(roundA, 3*u); hidden(roundA);
circle(roundB, (right,middle), 32*u); outlined(roundB); color(roundB, dim); stroke(roundB, 3*u); hidden(roundB);
circle(roundC, (right,lower), 32*u); outlined(roundC); color(roundC, dim); stroke(roundC, 3*u); hidden(roundC);
circle(r1, (left,middle), 13*u); color(r1, gold); hidden(r1);
rect(r2, (left,middle), 25*u,25*u); color(r2, magenta); hidden(r2);
text(r3, (left,middle), "◆"); size(r3, 34*u); color(r3, cyan); hidden(r3);
tag(cold2a, scene2structure); tag(cold2b, scene2structure); tag(cold2c, scene2structure);
tag(roundA, scene2structure); tag(roundB, scene2structure); tag(roundC, scene2structure);
tag(cold2a, scene2); tag(cold2b, scene2); tag(cold2c, scene2);
tag(hot2a, scene2); tag(hot2b, scene2); tag(hot2c, scene2);
tag(roundA, scene2); tag(roundB, scene2); tag(roundC, scene2);
tag(r1, scene2); tag(r2, scene2); tag(r3, scene2);
// Scene 3: parallel composition makes every authored route active.
line(cold3a, (left,middle), (right,upper)); dashed(cold3a); color(cold3a, dim); stroke(cold3a, 3*u); hidden(cold3a);
line(cold3b, (left,middle), (right,middle)); dashed(cold3b); color(cold3b, dim); stroke(cold3b, 3*u); hidden(cold3b);
line(cold3c, (left,middle), (right,lower)); dashed(cold3c); color(cold3c, dim); stroke(cold3c, 3*u); hidden(cold3c);
line(hot3a, (left,middle), (right,upper)); color(hot3a, cyan); stroke(hot3a, 7*u); untraced(hot3a);
line(hot3b, (left,middle), (right,middle)); color(hot3b, magenta); stroke(hot3b, 7*u); untraced(hot3b);
line(hot3c, (left,middle), (right,lower)); color(hot3c, gold); stroke(hot3c, 7*u); untraced(hot3c);
circle(allA, (right,upper), 32*u); outlined(allA); color(allA, cyan); stroke(allA, 4*u); hidden(allA);
circle(allB, (right,middle), 32*u); outlined(allB); color(allB, magenta); stroke(allB, 4*u); hidden(allB);
circle(allC, (right,lower), 32*u); outlined(allC); color(allC, gold); stroke(allC, 4*u); hidden(allC);
circle(b1, (left,middle), 13*u); color(b1, cyan); hidden(b1);
rect(b2, (left,middle), 25*u,25*u); color(b2, magenta); hidden(b2);
text(b3, (left,middle), "◆"); size(b3, 34*u); color(b3, gold); hidden(b3);
tag(cold3a, scene3structure); tag(cold3b, scene3structure); tag(cold3c, scene3structure);
tag(allA, scene3structure); tag(allB, scene3structure); tag(allC, scene3structure);
tag(cold3a, scene3); tag(cold3b, scene3); tag(cold3c, scene3);
tag(hot3a, scene3); tag(hot3b, scene3); tag(hot3c, scene3);
tag(allA, scene3); tag(allB, scene3); tag(allC, scene3);
tag(b1, scene3); tag(b2, scene3); tag(b3, scene3);
// Scene 4: this curve connects nothing. It exists only because the design wants motion.
spline(ribbon, (w*0.11,h*0.57), (w*0.24,h*0.38), (w*0.40,h*0.55), (w*0.59,h*0.34), (w*0.88,h*0.49));
color(ribbon, magenta); stroke(ribbon, 8*u); untraced(ribbon); tag(ribbon, scene4);
hidden(ribbon.knots);
circle(spark1, (w*0.24,h*0.37), 8*u); color(spark1, cyan); hidden(spark1); tag(spark1, scene4);
circle(spark2, (w*0.73,h*0.53), 8*u); color(spark2, gold); hidden(spark2); tag(spark2, scene4);
step("select-one") {
par {
draw(hot1b, 1.65, smooth);
flow(hot1b, 1.65);
travel(parcel, hot1b, 1.65, smooth);
}
}
wait(0.55);
step("creator-orders-the-motion") {
seq {
fade(scene1, 0.35);
par {
show(scene2structure, 0.40);
say(mode, "THREE OBJECTS · CREATOR-AUTHORED ORDER", 0.35);
say(caption, "Nothing inferred round robin. The creator simply places three motions in seq.", 0.40);
}
par { show(r1, 0.15); draw(hot2a, 0.75, smooth); flow(hot2a, 0.75); travel(r1, hot2a, 0.75, smooth); }
par { show(r2, 0.15); draw(hot2b, 0.75, smooth); flow(hot2b, 0.75); travel(r2, hot2b, 0.75, smooth); }
par { show(r3, 0.15); draw(hot2c, 0.75, smooth); flow(hot2c, 0.75); travel(r3, hot2c, 0.75, smooth); }
}
}
wait(0.55);
step("creator-sends-together") {
seq {
fade(scene2, 0.35);
par {
show(scene3structure, 0.40);
show(b1, 0.20); show(b2, 0.20); show(b3, 0.20);
say(mode, "THREE OBJECTS · THREE PATHS · TOGETHER", 0.35);
say(caption, "Nothing inferred broadcast. par makes every authored journey happen together.", 0.40);
}
par {
draw(hot3a, 1.40, smooth); flow(hot3a, 1.40); travel(b1, hot3a, 1.40, smooth);
draw(hot3b, 1.40, smooth); flow(hot3b, 1.40); travel(b2, hot3b, 1.40, smooth);
draw(hot3c, 1.40, smooth); flow(hot3c, 1.40); travel(b3, hot3c, 1.40, smooth);
}
}
}
wait(0.55);
step("motion-can-just-be-design") {
seq {
fade(scene3, 0.35);
par {
say(mode, "NO ENDPOINTS · NO DOMAIN · JUST MOTION", 0.35);
say(caption, "A free curve can carry light in both directions because the composition asks for it.", 0.40);
show(spark1, 0.25); show(spark2, 0.25);
draw(ribbon, 0.80, smooth);
}
flow(ribbon, 4.20, both, continuous);
}
}
wait(1.25);
step("foundation") {
par {
pulse(ribbon, 0.75);
say(mode, "OBJECTS · PATHS · MOTION · TIMING", 0.35);
say(caption, "Manic animates the creator's declaration. It never invents the subject's behavior.", 0.45);
}
}
wait(1.40);
Run it directly or audit every target format:
manic examples/motion-flow-foundation.manic
manic check examples/motion-flow-foundation.manic --canvas all
Complete V2 example
This generic Reel demonstrates all three relationship words, release, path travel, an identity-preserving blueprint change, particle arrangement, a shared pivot turn, and a readable final hold:
// motion-graphics-v2.manic — relationships, identity, and a deliberate settle
title("Motion Graphics V2 — Follow, Become, Turn");
canvas("9:16");
template("mono");
watermark(manicMark, (150, 132), "Made With Manic");
creator(me, "@anish2good name=Manic_Motion tagline=Describe_the_relationship yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social cta=Create_yours safe=reels");
socials(me);
text(kicker, (540, 155), "MOTION GRAPHICS V2");
size(kicker, 23); color(kicker, dim); bold(kicker);
text(headline, (540, 228), "Move the idea, not every coordinate");
size(headline, 40); bold(headline);
text(subhead, (540, 292), "attach · become · turn");
size(subhead, 25); color(subhead, dim);
rect(stage, (540, 865), 900, 1020);
color(stage, panel); outline(stage, dim); opacity(stage, 0.78);
// One authored route and one persistent object.
plot(route, (205, 1040), 172, 190, "0.55*sin(1.2*x)+0.18*x", (0, 4.25));
color(route, cyan); stroke(route, 6); untraced(route);
dot(marker, (205, 1040), 12);
color(marker, gold); glow(marker, 1.6);
text(markerLabel, (205, 994), "one persistent object");
size(markerLabel, 24); color(markerLabel, fg); hidden(markerLabel);
// A hidden target is a visual blueprint; it never appears as a second object.
circle(nodeBlueprint, (935, 885), 56);
color(nodeBlueprint, magenta); outlined(nodeBlueprint); stroke(nodeBlueprint, 7);
glow(nodeBlueprint, 1.2); hidden(nodeBlueprint);
// Supporting particles begin offstage, then arrive as an ordered ring.
rect(seedBox, (540, 610), 260, 180); hidden(seedBox);
particles(sparks, seedBox, 14, 7, 42, "random");
color(sparks, cyan); hidden(sparks);
circle(finalOrbit, (935, 885), 138); hidden(finalOrbit);
text(explain, (540, 1325), "The label follows because the relationship is authored.");
size(explain, 26); color(explain, dim); wrap(explain, 800); hidden(explain);
step("attach-and-travel") {
seq {
attach(markerLabel, marker, (0,-46));
par {
show(markerLabel, 0.35);
show(explain, 0.40);
draw(route, 2.8, smooth);
travel(marker, route, 2.8, smooth);
}
wait(0.45);
}
}
step("become-the-next-idea") {
seq {
par {
become(marker, nodeBlueprint, 1.05, smooth);
say(markerLabel, "same id · new visual state", 0.45);
say(explain, "become preserves identity and settles on the exact blueprint.", 0.45);
}
wait(0.50);
}
}
step("release-arrange-turn") {
seq {
attach(markerLabel, none);
par {
fade(markerLabel, 0.35);
fade(route, 0.45);
show(sparks, 0.30);
arrange(sparks, finalOrbit, "ring", 1.15, smooth);
say(explain, "Release the label. Arrange a system. Turn it around one pivot.", 0.45);
}
turn(sparks, marker, 24, 0.65, out);
wait(1.40);
}
}
Run it directly—no extra runtime option is required:
manic examples/motion-graphics-v2.manic
Advanced story — compose the whole motion language
The compact example above answers “what do the three new words do?” This advanced Reel answers the more important creator question: “how do they form a story with the motion vocabulary I already know?”
It keeps one visual world alive across three acts:
- A question travels while its label stays attached; a path pulse guides the eye and surrounding facts wander.
- The notation rewrites, the same question becomes a model, and the same facts arrange into a system. The model then spins locally.
- Labels and particles turn around one shared pivot while the equation reaches the story’s final meaning.
seq creates cause and effect, par groups changes that express one idea, and
stagger prevents a dense scene from arriving as a visual shock.
// motion-graphics-v2-story.manic — one idea, one continuous motion story
title("The Journey of an Idea — Motion Graphics V2 Story");
canvas("9:16");
template("mono");
watermark(manicMark, (170, 178), "Made With Manic");
creator(me, "@anish2good name=Manic_Motion tagline=Move_the_idea yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social cta=Create_yours safe=reels");
socials(me);
// ---------------------------------------------------------------------------
// ONE PERSISTENT STAGE
// The scene never cuts away. Each act reuses what the previous act established.
// ---------------------------------------------------------------------------
text(kicker, (540, 184), "A MOTION GRAPHICS STORY");
size(kicker, 22); color(kicker, dim); bold(kicker); hidden(kicker);
text(headline, (540, 248), "How an idea learns to move");
size(headline, 42); bold(headline); hidden(headline);
text(chapter, (540, 316), "ACT 1 · FOLLOW ATTENTION");
size(chapter, 24); color(chapter, cyan); bold(chapter); hidden(chapter);
rect(stage, (540, 850), 900, 990);
color(stage, panel); outline(stage, dim); opacity(stage, 0.76);
text(narration, (540, 1400), "A question appears before the answer does.");
size(narration, 28); color(narration, fg); wrap(narration, 820); hidden(narration);
text(principle, (540, 1495), "Motion should guide the eye, preserve meaning, then settle.");
size(principle, 22); color(principle, dim); wrap(principle, 820); hidden(principle);
// ---------------------------------------------------------------------------
// ACT 1 — CURIOSITY TRAVELS; ITS CONTEXT FOLLOWS
// ---------------------------------------------------------------------------
spline(questionPath, (175, 1080), (290, 980), (385, 1060), (490, 900), (540, 790));
color(questionPath, cyan); stroke(questionPath, 5); dashed(questionPath, 18, 15);
glow(questionPath, 0.8); untraced(questionPath);
dot(idea, (175, 1080), 12);
color(idea, gold); glow(idea, 1.7); hidden(idea);
text(focusLabel, (175, 1024), "WHY?");
size(focusLabel, 29); color(focusLabel, gold); bold(focusLabel); hidden(focusLabel);
// Facts begin as visual noise. They persist through every later arrangement.
rect(noiseField, (540, 820), 690, 470); hidden(noiseField);
particles(facts, noiseField, 16, 7, 29, "random");
color(facts, cyan); glow(facts, 0.9); hidden(facts);
for i in 0..16 {
tag(facts.p{i}, knowledgeSystem);
}
// ---------------------------------------------------------------------------
// ACT 2 — THE SAME QUESTION BECOMES A MODEL
// ---------------------------------------------------------------------------
equation(work, (540, 1195), `1+2+\cdots+n=?`, 43);
color(work, fg); hidden(work);
// Hidden entities are visual blueprints. `idea` adopts this exact final state.
rect(modelBlueprint, (540, 790), 116, 116);
outlined(modelBlueprint); outline(modelBlueprint, magenta); stroke(modelBlueprint, 7);
rot(modelBlueprint, 45); glow(modelBlueprint, 1.5); hidden(modelBlueprint);
circle(knowledgeOrbit, (540, 790), 250); hidden(knowledgeOrbit);
text(observe, (540, 492), "OBSERVE");
text(connect, (842, 790), "CONNECT");
text(testIdea, (540, 1088), "TEST");
text(explain, (238, 790), "EXPLAIN");
size(observe, 20); size(connect, 20); size(testIdea, 20); size(explain, 20);
color(observe, dim); color(connect, dim); color(testIdea, dim); color(explain, dim);
bold(observe); bold(connect); bold(testIdea); bold(explain);
hidden(observe); hidden(connect); hidden(testIdea); hidden(explain);
tag(observe, knowledgeSystem); tag(connect, knowledgeSystem);
tag(testIdea, knowledgeSystem); tag(explain, knowledgeSystem);
// ---------------------------------------------------------------------------
// ACT 3 — THE MODEL BECOMES A COORDINATED SYSTEM
// ---------------------------------------------------------------------------
text(systemLabel, (540, 648), "ONE IDEA · MANY RELATIONSHIPS");
size(systemLabel, 22); color(systemLabel, magenta); bold(systemLabel); hidden(systemLabel);
text(finalLine, (540, 1320), "Don't animate layers. Animate the relationship.");
size(finalLine, 30); color(finalLine, fg); bold(finalLine); wrap(finalLine, 820); hidden(finalLine);
// ---------------------------------------------------------------------------
// TIMELINE — seq tells the story; par and stagger control the viewing rhythm.
// ---------------------------------------------------------------------------
step("a-question-pulls-attention") {
seq {
par {
show(kicker, 0.35);
show(headline, 0.55);
show(chapter, 0.40);
show(narration, 0.45);
show(principle, 0.45);
show(idea, 0.30);
}
// Stagger turns a cloud into a readable arrival instead of one visual shock.
stagger(0.035) {
for i in 0..16 {
show(facts.p{i}, 0.22);
}
}
attach(focusLabel, idea, (0,-56));
par {
show(focusLabel, 0.30);
draw(questionPath, 2.65, smooth);
travel(idea, questionPath, 2.65, smooth);
flow(questionPath, 1.45);
wander(facts, 2.65);
}
par {
to(idea, scale, 1.55, 0.45, out);
say(narration, "Attention follows the question while the background stays alive.", 0.45);
}
wait(0.55);
}
}
step("the-question-becomes-a-model") {
seq {
attach(focusLabel, none);
par {
fade(focusLabel, 0.30);
fade(questionPath, 0.45);
say(chapter, "ACT 2 · PRESERVE THE IDEA", 0.35);
say(narration, "The question changes form, but it never loses its identity.", 0.45);
show(work, 0.40);
}
// Existing notation moves only where the mathematical meaning changes.
rewrite(work, `1+2+\cdots+n=\frac{n(n+1)}{2}`, 1.00, smooth);
par {
become(idea, modelBlueprint, 1.05, smooth);
arrange(facts, knowledgeOrbit, "ring", 1.20, smooth);
say(principle, "become keeps one id · arrange keeps every particle", 0.45);
}
// spin is local: the new model turns around its own centre.
spin(idea, 90, 0.65, out);
stagger(0.10) {
show(observe, 0.30);
show(connect, 0.30);
show(testIdea, 0.30);
show(explain, 0.30);
}
wait(0.55);
}
}
step("relationships-move-as-one") {
seq {
attach(systemLabel, idea, (0,-142));
par {
show(systemLabel, 0.35);
say(chapter, "ACT 3 · MOVE THE SYSTEM", 0.35);
say(narration, "Now every part can move together without losing the structure.", 0.45);
show(finalLine, 0.45);
}
// turn is relational: particles and labels share one pivot and one motion.
par {
turn(knowledgeSystem, idea, 45, 1.20, smooth);
spin(idea, -135, 1.20, smooth);
to(facts, color, lime, 1.20, smooth);
rewrite(work, `\text{question}\;\longrightarrow\;\text{model}\;\longrightarrow\;\text{insight}`, 1.20, smooth);
}
par {
pulse(idea, 0.70);
say(principle, "FOLLOW · TRANSFORM · COORDINATE · SETTLE", 0.45);
}
wait(1.80);
}
}
Run the advanced story through the same file-only production path:
manic examples/motion-graphics-v2-story.manic
The original essentials example remains useful when learning wander,
travel, explicit morph, and particle arrangement:
// motion-graphics.manic — persistent objects, paths, particles, and a clean settle
title("Motion Graphics Essentials");
canvas("9:16");
template("mono");
watermark(manicMark, (150, 145), "Made With Manic");
let accent = 194;
let warm = 38;
text(kicker, (540, 170), "MOTION GRAPHICS");
size(kicker, 24); color(kicker, dim);
text(headline, (540, 245), "Keep the object. Change its state.");
size(headline, 42);
rect(chamber, (540, 535), 700, 300);
outline(chamber, dim); outlined(chamber); stroke(chamber, 3);
rect(startCell, (350, 535), 290, 270); hidden(startCell);
particles(dots, startCell, 36, 6, 23);
hue(dots, accent, 0.58, 0.72); glow(dots, 0.7);
text(note, (540, 835), "random layout · organic deterministic routes");
size(note, 25); color(note, dim);
plot(path, (245, 1120), 145, 175, "1-exp(-1.4*x)", (0, 3.8));
hue(path, warm, 0.60, 0.70); stroke(path, 5); untraced(path);
dot(marker, (245, 1120), 9); color(marker, fg); glow(marker, 1.0);
text(pathNote, (540, 1215), "travel moves the real marker — then it stays");
size(pathNote, 25); color(pathNote, dim);
line(connector, (245, 1120), (835, 850)); hidden(connector);
plot(pathMorph, (245, 1120), 145, 175, "1-exp(-1.4*x)", (0, 3.8));
hue(pathMorph, warm, 0.60, 0.70); stroke(pathMorph, 5);
morph(pathMorph, connector); hidden(pathMorph);
circle(orbit, (540, 535), 245); hidden(orbit);
step("scatter") {
seq {
par {
seq { arrange(dots, chamber, "random", 1.25, out); wander(dots, 1.75); }
seq { wait(0.25); say(note, "same dots · natural motion · repeatable render", 0.45); }
}
}
}
step("follow-a-path") {
seq {
par {
draw(path, 2.4, smooth);
travel(marker, path, 2.4, smooth);
}
wait(0.45);
}
}
step("preserve-the-path") {
seq {
par {
fade(path, 0.20);
show(pathMorph, 0.10);
to(pathMorph, morph, 1, 0.90, smooth);
say(pathNote, "morph keeps an open path open", 0.45);
}
wait(0.45);
}
}
step("arrive-and-settle") {
seq {
par {
arrange(dots, orbit, "ring", 1.15, smooth);
fade(chamber, 0.35);
seq { wait(0.45); say(note, "ring arrival", 0.35); }
}
par {
transform(dots, (540,535), 0.9659, -0.2588, 0.2588, 0.9659, 0.50, out);
say(note, "brief motion → a deliberate stop", 0.35);
}
wait(1.20);
}
}
Professional motion checklist
- Author relationships and final states; avoid hand-keyframing intermediate coordinates.
- Use
parfor changes that belong to one idea andseqfor cause-and-effect. - Prefer
smoothfor explanatory transformations andoutfor a short settle. - Keep an important object id alive instead of fading it out and rebuilding it.
- Leave a final
waitso the audience can read the state motion created. - Preview by named
stepand scrub backwards: the same time must always produce the same frame.
Voice — narrate your story
Add spoken narration to a Manic video with three ideas:
- Pick a voice once —
voice("…") - Speak full sentences —
speak("…") - Keep short on-screen text separate —
say(…)
You do not time the audio by hand. Each speak line lasts as long as it
takes to say — motion on that beat waits with it.
Voice is optional. Skip it when the video should stay silent; use say alone
for captions.
say vs speak
say | speak | |
|---|---|---|
| What it does | Changes on-screen text | Speaks a voice-over line |
| Heard? | No | Yes |
Needs voice(...)? | No | Yes — once per file |
| Best for | Short punch lines, labels | Full sentences the viewer hears |
| Example | say(cap, "GOAL"); | speak("The ball curls into the net."); |
Rule of thumb: write the short caption with say, the full line with
speak. They can run together on the same beat.
par {
say(cap, "same grid → 3D"); // what you read
speak("Now lift the same grid into height."); // what you hear
}
Or update a caption and speak in one call:
speak(cap, "Pull the camera back.");
How to add narration (step by step)
1. Put a caption box on the cast (optional but common)
text(cap, (cx, h*0.82), "");
size(cap, 28); color(cap, dim); wrap(cap, w*0.8);
2. Choose a voice once (near the top)
voice("cartesia");
You only need this if you use speak. One voice(...) per file is enough.
3. Narrate on the timeline
speak("Every world starts flat.");
Together:
title("A spoken beat");
canvas("16:9");
voice("cartesia");
text(cap, (cx, h*0.82), "");
size(cap, 28); color(cap, dim); wrap(cap, w*0.8);
par {
say(cap, "cell by cell…");
speak("Watch a map generate itself until the islands lock.");
}
wait(0.3); // a short breath before the next idea
speak and voice always go together. Captions alone need only say — no
voice line.
Choosing a speaker
voice takes up to four pieces:
voice(service, speaker?, pace?, language?)
Easy names (recommended)
voice("gtts"); // simple built-in
voice("cartesia"); // Skylar
voice("cartesia", "katie");
voice("cartesia", "jameson");
voice("elevenlabs"); // Roger
voice("elevenlabs", "alice");
voice("elevenlabs", "jessica");
| service | easy speaker names |
|---|---|
| Cartesia | katie, skylar, jameson, gemma, archie |
| ElevenLabs | roger, alice, sarah, jessica, adam, george, liam, … |
Your own speaker (provider voice id)
Paste the voice id from Cartesia or ElevenLabs when you have a favourite speaker that is not in the easy list:
// Cartesia — voice UUID from your Cartesia voice library
voice("cartesia", "a0e99841-438c-4a64-b679-ae501e7d6091");
// ElevenLabs — voice id from your ElevenLabs voice library
voice("elevenlabs", "21m00Tcm4TlvDq8ikWAM");
Easy names and raw ids both go in the same speaker slot.
Pace
voice("elevenlabs", "alice", "slow"); // normal | slow | fast
voice("cartesia", "katie", "fast");
Language
Defaults to English (en). Pass a language code when the narration is not English:
// language last (with pace)
voice("cartesia", "katie", "normal", "hi");
voice("elevenlabs", "alice", "normal", "es");
// or skip pace and put the language third
voice("cartesia", "katie", "hi");
voice("elevenlabs", "roger", "fr");
Common codes: en, hi, es, fr, de, pt, ja, zh, ko, it, ar, …
For the built-in gtts service, the second argument is the language:
voice("gtts", "en");
voice("gtts", "hi");
Narrate with motion (same beat)
Put caption, voice, and animation in one par so they land together:
par {
say(cap, "fly the ridges");
speak("Fly in low over the ridges.");
orbit3(255, 15, 12, 3.8, smooth);
}
If the spoken line is longer than the motion, Manic holds the picture until
the line finishes. A trailing wait(0.3) is only a pause between ideas —
not a substitute for narration length.
Pacing — budget your Short by word count
You don’t set timings by hand, but you can predict how long a narrated video will run: a spoken line lasts about 2.5 words per second (≈150 words per minute — a natural narration pace).
| Target length | Budget for speak |
|---|---|
| 15s Short | ~35–40 words |
| 30s Short | ~75 words |
| 60s Short | ~150 words |
So a Short that feels right on paper can quietly balloon: 150 words of narration is a full minute, even if the script looks short. If a video runs long, trim the words first — the motion follows the voice, not the other way round.
Preview pacing without an API key. Manic estimates each speak line’s
length from its word count even when text-to-speech is offline or no key is
set (the line is silent, but the timeline still reserves the time). That means
--still <t> lands on the same beat it will at full voice — so you can
check timing and framing before spending a single TTS call:
manic monty-hall.manic --still 15.6 # the frame the viewer sees at 15.6s
A few practicalities:
- Each line is clamped to 0.8s–30s — one very long
speakwon’t stall forever, but split big paragraphs into separate lines anyway; they read better. wait(...)adds on top of the spoken time. Sincespeakalready paces the beat, keep waits small (wait(0.3)–wait(0.6)) — they’re breaths between ideas, not the pacing itself.- Real TTS duration replaces the estimate when a voice key is present; it lands
very close, so your
--stillpreview stays accurate. - Tip: to count your narration, add up the words inside every
speak("…")and divide by 2.5 for a rough total in seconds.
Math in spoken lines
Write symbols as you would on screen; Manic reads them naturally:
speak("The slope is √(tan θ).");
Examples in the gallery
- Roberto Carlos’ Banana Kick — story VO + captions
- Your Map Is a 3D World — map → 3D with narration
- The Shortest Path — vector maze + A* with problem→solve narration (ElevenLabs)
- Made of Emoji — a vector spectacle with promo VO (ElevenLabs)
- The Monty Hall Problem — probability puzzle told in emoji, problem→solution→CTA narration (ElevenLabs)
Quick checklist
- One
voice("…")near the top when you want narration - Full sentences in
speak("…") - Short on-screen lines in
say(cap, "…") - Use
par { … }when caption, voice, and motion should start together - Budget length by word count (~2.5 words/sec) — ~75 words ≈ 30s
- Preview any beat with
--still <t>(timing holds even with no TTS key) - No voice? Omit
voice/speak—sayalone is fine
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 |
| Change a particle state | particles(...,"grid") + `arrange(id,container,“random | grid |
| 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 |
| Move a real object on a path | travel(marker,path,dur,ease) | a plot marker, vehicle, token, or probe follows the authored path once and stops at its endpoint |
| 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.
Kits — domain knowledge, ordinary Manic composition
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.
ml
Small neural-network explanations whose displayed activations and probabilities come from the declared model rather than a staged effect:
network(net, (cx, cy), "3 6 4 3", "relu tanh softmax", 820, 350, 21);
forward(net, "0.15 0.92 0.38", 4.2, smooth);
loss(net, "1 0 0", crossentropy, 1.5, smooth);
backward(net, 3.2, smooth);
checkpoint(beforeUpdate, net);
update(net, 0.18, 2.3, smooth);
restore(net, beforeUpdate, 2.3, smooth); // exact rollback, not general unlearning
tensor(image, (260, 340), "0 0 1; 0 1 1; 0 0 1", 44);
kernel(edge, (560, 340), "-1 0 1; -2 0 2; -1 0 1", 44);
convolve(feature, image, edge, (850, 340), 1, 1, 0, relu, 44);
scan(feature, 4.0, smooth);
tokenize(words, (650, 150), "the cat chased the cat", word, 900);
embedding(context, words, (650, 470), "seeded 6 37", sinusoidal, 1080, 430);
transformer(block, context, (650, 500),
"heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41",
1120, 520);
encode(block, 6.2, smooth);
logits(next, block, 5, (650, 500),
"reason | predict | learn | adapt | explain | .", 0.8, 760, 440, 73);
sample(next, "top-p 0.90 seed=17", 3.8, smooth);
attention(head, (650, 360), "Art | ificial | intelligence | transforms | business",
"1 0.2 -0.4 0.7; 0.8 0.1 -0.3 0.6; -0.2 1 0.5 0.3; 0.1 0.6 0.9 -0.2; 0.7 -0.1 0.4 1",
980, 420, 23);
attend(head, 3, 5.2, smooth);
Use activation to introduce a scalar activation curve, then network and
forward to follow one computation. Add loss, backward, and update for
one explicit, numerically truthful learning step. Use tensor, kernel,
convolve, pool, and the shared scan for CNN/operator stories. Large layers
automatically reduce visual detail without reducing the numerical model. Use
tokenize and embedding to reveal honest token boundaries, stable lookup
vectors, exact positional values, and their elementwise sum. Use
transformer and encode for one complete multi-head block with masking,
normalization, residuals, MLP, and truthful training/inference dropout. Use
logits and sample to keep the separate LM projection, temperature-scaled
full softmax, filtering, renormalization, and seeded next-token choice truthful. Use
attention and attend for one focused, exact self-attention head; add topk
only when a deterministic educational candidate ranking helps the story. See
Machine learning — models made visible.
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.
chem
Real molecules, from real files — molecule3 reads an MDL structure file (a
PubChem Download → SDF, or a .mol) and lays out its atoms and bonds as ordinary
3-D entities. Nothing about the shape is authored: the coordinates are the file’s,
in Ångströms.
camera3((0, -14, 4), (0, 0, 0), 40);
molecule3(caf, "asset:molecules/caffeine.sdf"); // turns by default
color(caf.N, cyan); // every nitrogen
molecule3(bz, "asset:molecules/benzene.sdf", (6,0,0), 1.2, "style=sticks hydrogens=0");
Four styles (ball & stick, sticks, wireframe, space-filling), hydrogens on or off,
and a slow turn about its own axis on by default, because a shape only reads in
three dimensions. Atoms are CPK/Jmol coloured and sized by covalent radius; every
atom is tagged by its element, so color(caf.N, cyan) addresses the chemistry
rather than an index. 26 molecules bundled.
structure is the flat half: the skeletal formula from a file’s 2-D depiction
coordinates — carbons as bare vertices, OH/NH2 folded from the explicit
hydrogens, double bonds leaning into the ring, wedge and hash bonds where the file
records stereochemistry.
template("paper");
structure(glc, "asset:molecules/glucose-2d.sdf", (400, 360), 62);
untraced(glc.bonds);
stagger(0.05) { draw(glc.bonds, 0.4); } // written on, bond by bond
See the chemistry guide.
circuit
A netlist in, a working schematic out — Modified Nodal Analysis under easy builtins, so every voltage, current and glow on screen was solved, not drawn, and pre-simulated at build time so it scrubs and records exactly.
circuit(rc, (640, 360), `
dc-voltage 0 4 0 0 v=5
resistor 0 0 5 0 r=1k name=R1
capacitor 5 0 5 4 c=1u
wire 5 4 0 4
ground 0 4
`);
probe(rc, (5, 0)); // 63.2% of 5 V at one time constant
run(rc, 6); // draws itself, then charges
cut(rc, R1, 0.8); // take the resistor out …
run(rc, 3); // … and nothing flows
The topology comes from the geometry — coinciding points, or points joined by
a wire, are the same node, so there are no node numbers to write. The schematic
draws itself one component at a time and each prefix of the netlist is really
solved, so nothing flows until the loop closes. A lamp glows because it is
dissipating power. cut re-solves what is left from scratch, which is why
breaking a series loop darkens every lamp in it.
44 component types: passives and sources, six kinds of switch, junctions
(diode/led/zener), eight transistors (bipolar, MOSFET, JFET, Darlington),
opamp/comparator/schmitt/vco/transmission-line, and the logic family
down to flip-flops. current(id, speed, shape, color, size) is the animator’s
dial; probe and scope are the instruments. Everything is an ordinary tagged
entity, so framebox, zoom, cam and pulse do the presentation.
See the circuits guide for the end-to-end walkthrough.
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=maniclang.com 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://docs.maniclang.com, and you can see them all in motion in the Examples gallery.
Machine learning — models made visible
Manic ML begins with a simple promise: the learner should see the values the model actually computed, one meaningful flow at a time. A dense network is not useful merely because every node and edge is present. The current layer should be legible; the surrounding architecture should provide context without becoming a mesh of noise.
ML1 covers deterministic feed-forward networks and activation functions. ML2 adds supervised loss, exact reverse-mode gradients, and explicit parameter updates on the same persistent figure. ML3 adds tensors, convolution, pooling, and a shared operator scan. ML4 adds one exact, focused self-attention head and a deterministic top-k output view. ML5 makes the earlier text-to-model-input journey visible through honest token boundaries, embeddings, and position. ML6 carries those vectors through a complete multi-head transformer block. ML7 keeps the final language-model projection separate, then makes temperature and next-token sampling visible without pretending to run a pretrained model.
The visual language is still ordinary Manic
The ML nouns compute the diagram. Core Manic supplies the cinematography. The shipped examples use a small visual grammar consistently:
- cyan carries input or data;
- lime carries positive or retained signal;
- magenta carries negative contribution or reverse gradient;
- gold marks the active operation, residual result, or selected prediction;
- dim structure preserves context without letting every edge compete.
Attention outputs and Transformer stages contain real vector summaries or
signed mini bars, so a labelled box is never the only explanation. forward
leaves a quiet contribution-weighted trace, backward uses external gradient
badges, and encode flows through the main path plus both residual bypasses.
These are defaults of the kit; creators do not need to animate internal tags by
hand.
Camera motion is optional. When it helps, keep it small, focus one calculation, and return to the complete result:
step("compute") {
par {
forward(net, "0.15 0.92 0.38", 4.2, smooth);
seq {
par { cam((w*0.24,h*0.47),0.55,smooth); zoom(1.06,0.55,smooth); }
wait(0.55);
cam((cx,h*0.47),0.8,smooth);
wait(0.55);
cam((w*0.76,h*0.47),0.8,smooth);
par { cam((cx,cy),0.55,smooth); zoom(1,0.55,smooth); }
}
}
}
Avoid decorative particles around a network. Use a bounded packet or flow
only when it represents a token, activation, attention contribution, gradient,
or sampled choice. The settled frame must remain understandable with motion
paused and with the default black template.
Start with the story you need
You do not need to learn the whole ML kit. Choose one row, create the named
object, and use normal Manic step, show, say, and pulse calls around it.
| Story | Start with | Animate with |
|---|---|---|
| Scalar → vector → matrix → tensor | tensor | core Manic verbs |
| One activation function | activation | core Manic verbs |
| Prediction through a dense model | network | forward |
| Why a prediction is wrong | network + forward | loss |
| How gradients assign credit | prediction + loss | backward |
| One visible learning correction | completed backward pass | update |
| Image → feature map | tensor + kernel + convolve | scan |
| Smaller feature map | tensor + pool | scan |
| Text → tokens → positioned vectors | tokenize + embedding | core Manic verbs |
| How one token finds context | attention | attend |
| Context → candidate probabilities | attention + topk | core Manic verbs |
| Complete transformer block | transformer | encode |
| Hidden state → logits → next token | transformer + logits | sample |
The smallest useful pattern is:
activation(view, (cx,cy), relu, 510, 260);
untraced(view.axes);
untraced(view.curve);
step("meet-the-rule") {
draw(view.axes, 0.7);
draw(view.curve, 1.2);
}
ML words compute or construct the truthful figure. Core Manic remains the story language. This division keeps a tensor-only or activation-only lesson small instead of forcing every creator into a complete neural network.
The complete ML1–ML7 vocabulary
| Kind | Words | What the creator supplies |
|---|---|---|
| Figures | tensor, kernel, activation, network, tokenize, embedding, attention, transformer | values, shape, text, boundaries, activation names, embeddings, compact block options, or a deterministic seed |
| Derived views | convolve, pool, topk, logits | a source figure plus operator, candidate, token, temperature, or projection choices |
| Computation | forward, loss, backward, update, scan, attend, encode, sample | input, target, learning rate, selected token/block, decoding strategy, duration, and easing |
backward is intentionally not standalone: it requires a forward prediction
and loss. scan similarly requires a convolve or pool result. Errors name
the missing prerequisite instead of drawing a plausible but false animation.
Learn tensors without a network
Use a 1×1 grid for one displayed scalar, one row for a vector, rows for a
matrix, and | to stack channels. The accompanying title can name the familiar
rank while the same tensor noun supplies stable cells and values:
tensor(scalar, (180,360), "7");
tensor(vector, (460,360), "7 2 -1 4");
tensor(matrix, (760,360), "7 2 -1; 4 0 3; 1 5 6");
tensor(volume, (1060,360),
"7 2 -1; 4 0 3; 1 5 6 | 2 4 8; 1 3 9; 0 5 6");
Every part is selectable: volume.channel0, volume.row1, volume.col2,
volume.cells, volume.values, or one cell such as volume.c1.r0c2.
Explain one activation without a network
activation(view, (cx,cy), relu, 620, 320);
Standalone plots support linear, relu, sigmoid, and tanh. Use
view.axes, view.curve, and the ordinary line, point, equation, draw, and
pulse tools to test inputs or explain a region. softmax belongs inside a
network because it transforms a complete vector rather than one scalar.
A complete forward pass
network(net, (cx, cy), "3 6 4 3", "relu tanh softmax", 820, 350, 21);
forward(net, "0.15 0.92 0.38", 4.2, smooth);
The two quoted lists serve different purposes:
"3 6 4 3"defines input, hidden, hidden, and output layer sizes."relu tanh softmax"defines what happens after each of the three affine transitions.21is the deterministic seed. The same file produces the same weights, activations, prediction, and frames every time.- The input supplied to
forwardmust contain exactly three finite values.
Manic uses Xavier-uniform weights for the seeded educational model. It computes each affine layer, applies the named activation, and uses a numerically stable softmax. The output bars and percentages are derived from that result.
One complete learning step
forward(net, "0.15 0.92 0.38", 3.2, smooth);
loss(net, "1 0 0", crossentropy, 1.5, smooth);
backward(net, 3.2, smooth);
checkpoint(beforeUpdate, net);
update(net, 0.18, 2.3, smooth);
These four beats deliberately remain visible:
forwardstores the real activations for the authored input.losscompares the output with the target.crossentropyrequires a softmax output and a non-negative target distribution that sums to one.msesupports other output activations and arbitrary finite targets.backwardcalculates exact reverse-mode gradients for every visible and hidden parameter. The pulse travels output → input along the existing edges.updateappliesparameter -= learning_rate * gradient, recomputes the same input, and replaces the output bars and loss with their new computed values.
The optional loss kind defaults to cross-entropy for a softmax output and MSE
otherwise. The default learning rate is 0.15. A learning rate is not a visual
speed: changing it changes the mathematics, and a large value can truthfully
increase the loss.
Undo one authored update exactly
Place a checkpoint after the prediction has been compared with its target and before the parameter update. It takes no timeline time:
backward(net, 3.2, smooth);
checkpoint(beforeUpdate, net);
update(net, 0.18, 2.3, smooth);
restore(net, beforeUpdate, 2.3, smooth);
checkpoint saves every weight and bias plus the current layer values,
prediction, target, and loss. restore reverses the visible flow and returns
all of them to that exact saved state. It also clears active gradients; call
backward again before attempting another update.
This is precise checkpoint rollback. It is useful for explaining what one gradient step changed, comparing before and after, or showing an undo action. It is not general machine unlearning: restoring a saved state does not prove that a data point’s influence was removed from an otherwise trained model.
From pixels to feature maps
Rows use ;, values use spaces or commas, and channels use | inside one
quoted grid. This keeps small textbook tensors readable:
tensor(image, (250, 340), "0 0 1; 0 1 1; 0 0 1", 44, cyan);
kernel(edge, (540, 340), "-1 0 1; -2 0 2; -1 0 1", 44, magenta);
convolve(feature, image, edge, (820, 340), 1, 1, 0, relu, 44);
scan(feature, 4.0, smooth);
pool(compact, feature, (1080, 340), max, 2, 2, 0, 44);
scan(compact, 2.8, smooth);
convolve computes one output feature map. Its optional values are stride,
zero padding, bias, cellwise activation, and cell size. A multi-channel input
uses one kernel grid per input channel and sums every channel into each output
cell:
tensor(rgb, (300, 340), "1 2; 3 4 | 10 20; 30 40 | 2 0; 1 3");
kernel(k, (600, 340), "1 | 0.5 | -1");
convolve(feature, rgb, k, (900, 340));
For multiple feature detectors, author multiple kernels and outputs. That keeps each receptive field explainable instead of hiding a filter bank behind one visually dense call.
pool supports max and average, operating independently on each channel.
The default window is 2 and the default stride equals the window. Padding does
not fabricate candidate values: padded positions are excluded. Max-pool ties
select the first valid cell in row-major order, which makes selection stable
across renders and direct seeking.
One scanner for convolution and pooling
scan(output, duration, easing) coordinates four identities that should never
drift apart:
- the receptive-field frame on the source;
- the kernel/operator focus;
- the truthful arithmetic summary;
- the destination frame and exact revealed value.
When a pooled tensor consumes a convolution result, starting the pooling scan
automatically quiets the completed convolution arithmetic strip. The figures
remain in place, so the learner sees continuity without two competing status
lines. Use normal step, caption, show, and pulse calls for the narrative;
let scan own the synchronized numerical choreography.
Show how words gain position
Start with the sentence, choose honest boundaries, then turn those identities into vectors:
tokenize(words, (cx, 150), "the cat chased the cat", word, w*0.70);
embedding(context, words, (cx, 470), "seeded 6 37", sinusoidal,
w*0.90, h*0.46);
The optional token mode is:
word— Unicode letters/numbers form words; punctuation stays separate;character— each character is a token and whitespace remains visible;authored—|marks every exact boundary for a hand-authored subword explanation.
Authored boundaries are not called BPE because Manic has not applied a merge
table. embedding accepts either one explicit numeric row per token (rows
separated with ;) or "seeded DIM [SEED]". Seeded values are reproducible
educational lookup vectors, not pretrained weights. Repeated copies of the
same token reuse the same base vector; adding exact sinusoidal position makes
their final model inputs different. Use none instead of sinusoidal when the
lesson should stop at the lookup table.
Reveal context.vectors, then context.positions, then context.combined.
For comparison, pulse stable rows such as context.row1; use .dimN to focus
one feature across the table. Manic caps the story at 12 tokens and eight
dimensions so the values remain teachable on a phone.
Explain one transformer attention head
Give attention a short token list and one embedding row per token. Then focus
one token with its 1-based position:
attention(head, (cx, 360),
"Art | ificial | intelligence | transforms | business",
"1 0.2 -0.4 0.7; 0.8 0.1 -0.3 0.6; -0.2 1 0.5 0.3; 0.1 0.6 0.9 -0.2; 0.7 -0.1 0.4 1",
980, 420, 23);
attend(head, 3, 5.2, smooth);
This computes one seeded Q/K/V projection, the scaled score matrix
QK^T / sqrt(d), a stable row-wise softmax, and each exact weighted value mix.
attend(head, 3, ...) highlights the query for intelligence; it does not
rebuild or replace the surrounding tokens.
Add a small output ranking only when the story needs it:
topk(next, head, 3, (1540, 400),
"business | work | world | industry | future | people",
4, 420, 260, 29);
topk adds the selected embedding to its attention output, applies a seeded
educational output projection, and shows probabilities from the full softmax.
Those percentages are mathematically exact for the authored figure, but they
are not predictions from a pretrained language model. Keep the candidate list
small enough to read; the visual shows at most eight selected results.
Walk through a complete transformer block
Pass the ML5 embedding directly into transformer; do not copy its rows:
transformer(block, context, (cx, 500),
"heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41",
w*0.92, h*0.62);
encode(block, 6.2, smooth);
The one configuration sentence controls the choices that change the actual calculation:
heads=1..4;d_modelmust divide exactly across them;mask=none|causal; causal future cells receive zero probability;mlp=WIDTHup to 32 andactivation=gelu|relu|silu|tanh;norm=pre|post, which changes where both layer normalizations happen;dropout=0..less-than-1,mode=inference|training, and a reproducibleseed.
Each head computes scaled Q/K scores, applies its mask, normalizes with stable
softmax, and mixes V. Manic concatenates the heads, applies the output
projection, follows the first residual/norm stage, expands and contracts the
MLP, then follows the second residual/norm stage. Training dropout is a real
seeded boolean mask with inverted scaling; inference disables it completely.
encode reveals this existing computation and remains safe under direct seek.
Turn a hidden state into one next token
The transformer’s MLP produces another hidden representation. It does not
directly produce vocabulary probabilities. logits makes the separate
language-model head explicit:
logits(next, block, 5, (cx, 520),
"reason | predict | learn | adapt | explain | .",
0.8, 760, 440, 73);
sample(next, "top-p 0.90 seed=17", 3.8, smooth);
The third argument is a 1-based transformer token. Candidate labels are
|-separated and intentionally authored; Manic supports 2–12 in one readable
view. The optional temperature defaults to 1, followed by width, height, and
projection seed.
logits computes W_lm h + b from that final hidden row. It then divides every
logit by the positive temperature and applies one numerically stable softmax to
the complete candidate list. Reuse the same projection seed for a fair
temperature comparison: logits remain identical while every probability is
recomputed. Lower temperature sharpens the distribution; higher temperature
spreads it.
sample keeps four common choices behind one word:
| Strategy string | Exact behavior |
|---|---|
"greedy" | selects the highest probability; the decoding distribution is one-hot |
"categorical seed=17" | samples from the complete temperature-scaled distribution |
"top-k 3 seed=17" | keeps exactly the three highest candidates, zeros the rest, then renormalizes |
"top-p 0.90 seed=17" | keeps the smallest descending prefix reaching 90% mass, zeros the rest, then renormalizes |
An excluded candidate has exact probability zero and cannot be sampled. The same seed and same distribution choose the same result. All displayed values come from the deterministic educational projection declared in the scene; they are not predictions from a pretrained language model.
Introduce an activation first
activation(reluView, (cx, cy), relu, 510, 260);
untraced(reluView.axes);
untraced(reluView.curve);
par {
draw(reluView.axes, 0.7);
draw(reluView.curve, 1.2);
}
activation supports linear, relu, sigmoid, and tanh. A standalone
softmax curve would be misleading because softmax depends on all entries in a
vector; show it as the output activation of a network instead.
Design details that make the result readable
- Inactive edges remain quiet. During
forward, contribution magnitude drives emphasis and one pulse travels in the direction of computation. - Weight sign and magnitude affect edge styling, but labels, brightness, and width keep the structure understandable under the monochrome template.
- Large numerical layers show their first and last units around an ellipsis. Computation still uses every unit; only the drawing uses level of detail.
- Input, hidden, and output nodes retain stable ids throughout the story. Manic updates values instead of clearing and rebuilding the network.
- Output bars grow from zero to the computed value. A softmax output is labelled as a percentage and the status strip names the selected class.
lossplaces the target beside each output without replacing the prediction. Error magnitude focuses attention on the outputs that disagree.backwardtemporarily recolours connections by gradient sign and weights their emphasis by gradient magnitude. The settled network remains the same object, ready for the update.updateshows the gradient direction first, then restores edge styling from the new weights and recomputes every node. Its final status preserves the old and new loss so the claimed learning outcome is inspectable.restoresends one readable reverse pulse through the same graph, settles edge styling from the saved weights, and restores the saved output bars and loss without rebuilding the network.
Compose it like any other Manic scene
Every part is an ordinary entity carrying useful tags:
| Tag | Selects |
|---|---|
net | the complete network figure |
net.nodes, net.edges, net.values | one visual role |
net.layer0, net.layer1, … | one layer |
net.input, net.hidden, net.output | semantic layer groups |
net.probabilities | output bars and readouts |
net.loss | supervised target readouts |
image.cells, image.values, image.labels | tensor visual roles |
image.channel0, image.row0, image.col0 | tensor slices |
image.c0.r0c0 and .value | one cell and its numeric text |
feature.scan | receptive-field/operator/destination overlays |
words.source, .tokens, .indices, .tokenN | tokenization stages |
context.vectors, .positions, .combined | embedding addition stages |
context.rowN, .dimN, .operators | one token, feature, or operator |
block.heads, .headN, .mask, .matrix | multi-head attention and masks |
block.concat, .projection, .residual1, .norm1 | first half of the block |
block.mlp, .activation, .dropout, .residual2, .norm2 | second half |
block.input, .output, .tokenN, .rowN | persistent token lanes |
head.tokens, .q, .k, .v, .matrix | attention stages |
head.connections, .outputs, .residual | attention flow and residual lanes |
next.labels, .bars, .probabilities | top-k output roles |
That means normal verbs still apply:
hidden(net);
step("meet-the-model") {
show(net, 0.6);
}
step("compute") {
forward(net, "0.15 0.92 0.38", 4.2, smooth);
}
step("decision") {
pulse(net.output, 0.7);
}
Use named steps for question, intuition, computation, and takeaway. Let
forward own the dense numerical choreography; use captions and equations to
explain why the active operation matters.
For a learning story, keep the causal order clear:
step("predict") { forward(net, "0.15 0.92 0.38", 3.2); }
step("compare") { loss(net, "1 0 0", crossentropy, 1.5); }
step("credit") { backward(net, 3.2); }
checkpoint(beforeUpdate, net);
step("learn") { update(net, 0.18, 2.3); }
step("unlearn") { restore(net, beforeUpdate, 2.3); }
Calling forward with a new input starts a fresh learning beat and clears the
old target/gradient state. After update, another backward may compute fresh
gradients for the updated parameters and the same retained target. Every
update still requires a preceding backward; there is no invisible optimizer
loop. Name a restore step “rollback” or explain its exact boundary if you use
the playful label “unlearn”.
Current boundary
The ML kit is intentionally for small educational models. It does not load arbitrary PyTorch or TensorFlow programs, run hidden training loops, expose an optimizer catalogue, train large models, or require a GPU. Explicit authored weights, automatic filter banks, convolutional back-propagation, stacks of multiple transformer blocks, model imports, and packaged pretrained tokenizers remain planned work. ML4 deliberately computes one educational attention head, not a hidden pretrained language model. ML5 offers deterministic word/character splitting and exact authored boundaries; it does not claim a heuristic is BPE. Token sequences accept at most 12 tokens and eight embedding values each. Attention accepts 2–8 tokens with at most eight embedding values each; its candidate vocabulary is capped at 16. ML6 accepts 1–4 heads, requires exact division of the model dimension, caps the MLP at 32 values, and computes one educational block rather than an imported language model. ML7 accepts 2–12 authored candidates and a positive finite temperature; its LM projection and sampling seed are educational and explicit. Tensor axes are capped at 16 cells and a tensor at 2,048 values so unreadable stories fail early instead of silently becoming visual noise.
Review the shipped stories
The foundation story proves that tensor plus ordinary Manic is enough to
explain rank and dimensional growth:
// manic-ml-scalar-to-tensor.manic — ML foundation story
// One value gains an axis, then rows, then stacked channels. The ML-specific
// surface is only tensor(...); ordinary Manic verbs own the explanation.
title("Manic ML — From Scalar to Tensor");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Build_the_dimensions safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
let cell = 38*u;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · DATA FOUNDATIONS");
text(headline, (cx, h*0.105), "One value gains dimensions");
text(caption, (cx, h*0.84), "Begin with one measured value.");
tensor(scalar, (cx, h*0.20), "7", cell, gold);
tensor(vector, (cx, h*0.35), "7 2 -1 4", cell, cyan);
tensor(matrix, (cx, h*0.53), "7 2 -1; 4 0 3; 1 5 6", cell, magenta);
tensor(volume, (cx, h*0.72), "7 2 -1; 4 0 3; 1 5 6 | 2 4 8; 1 3 9; 0 5 6", cell, lime);
text(scalarTitle, (cx, h*0.155), "SCALAR · RANK 0");
text(vectorTitle, (cx, h*0.295), "VECTOR · RANK 1");
text(matrixTitle, (cx, h*0.445), "MATRIX · RANK 2");
text(volumeTitle, (cx, h*0.625), "TENSOR · RANK 3");
arrow(grow1, (cx, h*0.245), (cx, h*0.285));
arrow(grow2, (cx, h*0.395), (cx, h*0.435));
arrow(grow3, (cx, h*0.585), (cx, h*0.615));
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · DATA FOUNDATIONS");
text(headline, (cx, h*0.14), "One value gains dimensions");
text(caption, (cx, h*0.82), "Begin with one measured value.");
tensor(scalar, (w*0.11, h*0.46), "7", cell, gold);
tensor(vector, (w*0.34, h*0.46), "7 2 -1 4", cell, cyan);
tensor(matrix, (w*0.61, h*0.46), "7 2 -1; 4 0 3; 1 5 6", cell, magenta);
tensor(volume, (w*0.86, h*0.46), "7 2 -1; 4 0 3; 1 5 6 | 2 4 8; 1 3 9; 0 5 6", cell, lime);
text(scalarTitle, (w*0.11, h*0.34), "SCALAR · RANK 0");
text(vectorTitle, (w*0.34, h*0.34), "VECTOR · RANK 1");
text(matrixTitle, (w*0.61, h*0.34), "MATRIX · RANK 2");
text(volumeTitle, (w*0.86, h*0.34), "TENSOR · RANK 3");
arrow(grow1, (w*0.17, h*0.46), (w*0.23, h*0.46));
arrow(grow2, (w*0.44, h*0.46), (w*0.51, h*0.46));
arrow(grow3, (w*0.70, h*0.46), (w*0.76, h*0.46));
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · DATA FOUNDATIONS");
text(headline, (cx, h*0.17), "One value gains dimensions");
text(caption, (cx, h*0.84), "Begin with one measured value.");
tensor(scalar, (w*0.24, h*0.34), "7", cell, gold);
tensor(vector, (w*0.73, h*0.34), "7 2 -1 4", cell, cyan);
tensor(matrix, (w*0.24, h*0.65), "7 2 -1; 4 0 3; 1 5 6", cell, magenta);
tensor(volume, (w*0.73, h*0.65), "7 2 -1; 4 0 3; 1 5 6 | 2 4 8; 1 3 9; 0 5 6", cell, lime);
text(scalarTitle, (w*0.24, h*0.235), "SCALAR · RANK 0");
text(vectorTitle, (w*0.73, h*0.235), "VECTOR · RANK 1");
text(matrixTitle, (w*0.24, h*0.505), "MATRIX · RANK 2");
text(volumeTitle, (w*0.73, h*0.505), "TENSOR · RANK 3");
arrow(grow1, (w*0.34, h*0.34), (w*0.50, h*0.34));
arrow(grow2, (w*0.73, h*0.40), (w*0.36, h*0.56));
arrow(grow3, (w*0.35, h*0.65), (w*0.50, h*0.65));
}
size(kicker, 19*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 34*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 21*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
size(scalarTitle, 17*u); bold(scalarTitle); color(scalarTitle, dim); hidden(scalarTitle);
size(vectorTitle, 17*u); bold(vectorTitle); color(vectorTitle, dim); hidden(vectorTitle);
size(matrixTitle, 17*u); bold(matrixTitle); color(matrixTitle, dim); hidden(matrixTitle);
size(volumeTitle, 17*u); bold(volumeTitle); color(volumeTitle, dim); hidden(volumeTitle);
color(grow1, dim); stroke(grow1, 2.5*u); hidden(grow1);
color(grow2, dim); stroke(grow2, 2.5*u); hidden(grow2);
color(grow3, dim); stroke(grow3, 2.5*u); hidden(grow3);
hidden(scalar.cells); hidden(scalar.values); hidden(scalar.labels);
hidden(vector.cells); hidden(vector.values); hidden(vector.labels);
hidden(matrix.cells); hidden(matrix.values); hidden(matrix.labels);
hidden(volume.cells); hidden(volume.values); hidden(volume.labels);
step("scalar") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(scalarTitle, 0.30);
par {
show(scalar.cells, 0.55);
show(scalar.values, 0.55);
}
}
wait(0.55);
step("vector") {
par {
show(grow1, 0.35);
show(vectorTitle, 0.30);
show(vector.cells, 0.60);
show(vector.values, 0.60);
say(caption, "Repeat the value along one ordered axis: position now matters.", 0.40);
}
}
wait(0.60);
step("matrix") {
par {
show(grow2, 0.35);
show(matrixTitle, 0.30);
show(matrix.cells, 0.65);
show(matrix.values, 0.65);
say(caption, "Add rows to the columns: one axis becomes a two-dimensional matrix.", 0.40);
}
}
wait(0.65);
step("tensor") {
par {
show(grow3, 0.35);
show(volumeTitle, 0.30);
show(volume.cells, 0.70);
show(volume.values, 0.70);
say(caption, "Stack matrices as channels: the same values now carry depth and context.", 0.40);
}
}
wait(0.75);
step("one-family") {
pulse(volume.channel1, 0.75);
say(caption, "Scalar, vector, and matrix are all tensors—distinguished by their axes.", 0.45);
}
wait(1.50);
The activation story uses one real ReLU plot and core Manic probes to explain negative and positive inputs:
// manic-ml-activation-focus.manic — one ML noun, one complete lesson
// `activation` supplies the truthful ReLU curve. Core Manic supplies the
// question, equation, input probes, local emphasis, timing, and takeaway.
title("Manic ML — Why ReLU Changes a Neuron");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=See_the_rule safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · ACTIVATION");
text(headline, (cx, h*0.11), "Why does a neuron need ReLU?");
text(caption, (cx, h*0.79), "A neuron first receives an unrestricted number.");
equation(rule, (cx, h*0.60), `\operatorname{ReLU}(x)=\max(0,x)`, 35*u);
activation(reluView, (cx, h*0.38), relu, w*0.74, h*0.22);
line(negativePath, (w*0.13, h*0.466), (cx, h*0.466));
line(positivePath, (cx, h*0.466), (w*0.87, h*0.27));
line(positiveGuide, (w*0.685, h*0.466), (w*0.685, h*0.368));
circle(negativeProbe, (w*0.315, h*0.466), 8*u);
circle(positiveProbe, (w*0.685, h*0.368), 8*u);
text(negativeNote, (w*0.27, h*0.525), "x = −2 → 0");
text(positiveNote, (w*0.73, h*0.525), "x = 2 → 2");
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · ACTIVATION");
text(headline, (cx, h*0.14), "Why does a neuron need ReLU?");
text(caption, (cx, h*0.82), "A neuron first receives an unrestricted number.");
equation(rule, (w*0.79, h*0.36), `\operatorname{ReLU}(x)=\max(0,x)`, 34*u);
activation(reluView, (w*0.38, h*0.49), relu, w*0.52, h*0.48);
line(negativePath, (w*0.12, h*0.677), (w*0.38, h*0.677));
line(positivePath, (w*0.38, h*0.677), (w*0.64, h*0.25));
line(positiveGuide, (w*0.51, h*0.677), (w*0.51, h*0.463));
circle(negativeProbe, (w*0.25, h*0.677), 8*u);
circle(positiveProbe, (w*0.51, h*0.463), 8*u);
text(negativeNote, (w*0.79, h*0.50), "x = −2 → 0");
text(positiveNote, (w*0.79, h*0.59), "x = 2 → 2");
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · ACTIVATION");
text(headline, (cx, h*0.145), "Why does a neuron need ReLU?");
text(caption, (cx, h*0.82), "A neuron first receives an unrestricted number.");
equation(rule, (cx, h*0.67), `\operatorname{ReLU}(x)=\max(0,x)`, 34*u);
activation(reluView, (cx, h*0.40), relu, w*0.72, h*0.28);
line(negativePath, (w*0.14, h*0.509), (cx, h*0.509));
line(positivePath, (cx, h*0.509), (w*0.86, h*0.26));
line(positiveGuide, (w*0.68, h*0.509), (w*0.68, h*0.384));
circle(negativeProbe, (w*0.32, h*0.509), 8*u);
circle(positiveProbe, (w*0.68, h*0.384), 8*u);
text(negativeNote, (w*0.27, h*0.58), "x = −2 → 0");
text(positiveNote, (w*0.73, h*0.58), "x = 2 → 2");
}
size(kicker, 19*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 34*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 21*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
hidden(rule);
color(negativePath, magenta); stroke(negativePath, 5*u); glow(negativePath, 0.35); untraced(negativePath);
color(positivePath, cyan); stroke(positivePath, 5*u); glow(positivePath, 0.35); untraced(positivePath);
color(positiveGuide, dim); stroke(positiveGuide, 2*u); dashed(positiveGuide, 9*u, 7*u); untraced(positiveGuide);
color(negativeProbe, magenta); filled(negativeProbe); glow(negativeProbe, 0.55); hidden(negativeProbe);
color(positiveProbe, cyan); filled(positiveProbe); glow(positiveProbe, 0.55); hidden(positiveProbe);
size(negativeNote, 18*u); color(negativeNote, magenta); bold(negativeNote); hidden(negativeNote);
size(positiveNote, 18*u); color(positiveNote, cyan); bold(positiveNote); hidden(positiveNote);
untraced(reluView.axes);
untraced(reluView.curve);
hidden(reluView.label);
step("question") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
draw(reluView.axes, 0.75);
}
wait(0.55);
step("rule") {
show(rule, 0.55);
say(caption, "ReLU makes one transparent promise: return the larger of zero and x.", 0.40);
}
wait(0.55);
step("negative-input") {
par {
draw(negativePath, 0.75);
show(negativeProbe, 0.35);
show(negativeNote, 0.35);
say(caption, "Negative input is muted at zero; it cannot send negative evidence onward.", 0.40);
}
}
wait(0.65);
step("positive-input") {
par {
draw(positivePath, 0.85);
draw(positiveGuide, 0.55);
show(positiveProbe, 0.35);
show(positiveNote, 0.35);
say(caption, "Positive input passes through unchanged, preserving its strength.", 0.40);
}
}
wait(0.70);
step("activation") {
par {
draw(reluView.curve, 1.10);
show(reluView.label, 0.35);
say(caption, "That small bend gives a network a nonlinear decision boundary.", 0.40);
par { cam((cx, h*0.47), 0.70, smooth); zoom(1.10, 0.70, smooth); }
}
}
wait(0.75);
step("takeaway") {
par {
pulse(reluView.curve, 0.75);
say(caption, "ReLU does not invent a signal: it gates what the neuron already computed.", 0.45);
par { cam((cx, cy), 0.70, smooth); zoom(1.0, 0.70, smooth); }
}
}
wait(1.50);
The gallery example combines activation, network, forward computation, named story stages, Creator branding, and a professional restrained layout:
// manic-ml-forward-pass.manic — ML1 creator proof
// A real deterministic network computes one prediction. The story keeps the
// model persistent and reveals only the active computation instead of flashing
// every connection at once.
title("Manic ML — A Forward Pass You Can Follow");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=See_the_computation safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
// One story reflows to portrait, feed, square, and landscape. The portrait
// network narrows to reserve a clean probability lane at the right.
if h > 1.45*w {
watermark(manicMark, (w*0.18, h*0.075), "Made With Manic");
text(kicker, (cx, h*0.08), "MANIC ML · FORWARD PASS");
text(headline, (cx, h*0.13), "How does a network choose?");
text(caption, (cx, h*0.72), "First, a neuron keeps positive evidence and removes negative evidence.");
activation(reluView, (cx, h*0.40), relu, w*0.72, h*0.24);
network(model, (cx, h*0.42), "3 6 4 3", "relu tanh softmax", w*0.55, h*0.32, 21);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.18, h*0.10), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · FORWARD PASS");
text(headline, (cx, h*0.14), "How does a network choose?");
text(caption, (cx, h*0.83), "First, a neuron keeps positive evidence and removes negative evidence.");
activation(reluView, (cx, h*0.49), relu, w*0.42, h*0.36);
network(model, (cx, h*0.47), "3 6 4 3", "relu tanh softmax", w*0.64, h*0.39, 21);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · FORWARD PASS");
text(headline, (cx, h*0.13), "How does a network choose?");
text(caption, (cx, h*0.82), "First, a neuron keeps positive evidence and removes negative evidence.");
activation(reluView, (cx, h*0.43), relu, w*0.68, h*0.30);
network(model, (cx, h*0.45), "3 6 4 3", "relu tanh softmax", w*0.64, h*0.40, 21);
}
size(kicker, 20*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 36*u); bold(headline); hidden(headline);
size(caption, 22*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
// A truthful ReLU curve introduces the operation used by the hidden layer.
hidden(reluView);
untraced(reluView.axes);
untraced(reluView.curve);
// 3 inputs → 6 ReLU units → 4 tanh units → 3 softmax probabilities.
// Seed 21 makes the educational model reproducible across every render.
hidden(model);
step("activation") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(reluView, 0.55);
draw(reluView.axes, 0.70);
draw(reluView.curve, 1.20);
}
wait(0.75);
step("meet-the-network") {
fade(reluView, 0.45);
show(model, 0.65);
say(caption, "The same operation now lives inside a small, deterministic network.", 0.45);
}
wait(0.65);
step("forward-pass") {
par {
forward(model, "0.15 0.92 0.38", 4.20, smooth);
say(caption, "Follow the bright path: inputs become evidence, then probabilities.", 0.45);
seq {
par { cam((w*0.24, h*0.47), 0.55, smooth); zoom(1.06, 0.55, smooth); }
wait(0.55);
cam((cx, h*0.47), 0.80, smooth);
wait(0.55);
cam((w*0.76, h*0.47), 0.80, smooth);
wait(0.40);
par { cam((cx, cy), 0.55, smooth); zoom(1.0, 0.55, smooth); }
}
}
}
wait(0.90);
step("takeaway") {
pulse(model.output, 0.75);
say(caption, "The picture is driven by the computed values—not a decorative animation.", 0.45);
}
wait(1.60);
The ML2 story keeps one network on screen through prediction, target, loss, backward credit assignment, and a visibly recomputed gradient update:
// manic-ml-learning-step.manic — ML2 creator proof
// One persistent network predicts, measures its mistake, sends exact gradients
// backward, changes its parameters, then rolls that one saved change back.
// No layer is cleared and redrawn; rollback is not claimed as full unlearning.
title("Manic ML — How One Mistake Becomes Learning");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=See_the_learning safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.13, h*0.075), "Made With Manic");
text(kicker, (cx, h*0.08), "MANIC ML · ONE LEARNING STEP");
text(headline, (cx, h*0.13), "How does a model learn?");
text(caption, (cx, h*0.72), "First the network predicts from the current weights.");
network(model, (cx, h*0.42), "3 6 4 3", "relu tanh softmax", w*0.55, h*0.32, 21);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.18, h*0.10), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · ONE LEARNING STEP");
text(headline, (cx, h*0.14), "How does a model learn?");
text(caption, (cx, h*0.83), "First the network predicts from the current weights.");
network(model, (cx, h*0.47), "3 6 4 3", "relu tanh softmax", w*0.64, h*0.39, 21);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · ONE LEARNING STEP");
text(headline, (cx, h*0.13), "How does a model learn?");
text(caption, (cx, h*0.82), "First the network predicts from the current weights.");
network(model, (cx, h*0.45), "3 6 4 3", "relu tanh softmax", w*0.64, h*0.40, 21);
}
size(kicker, 20*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 35*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 22*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
hidden(model);
step("question") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(model, 0.65);
}
wait(0.55);
step("predict") {
par {
forward(model, "0.15 0.92 0.38", 3.20, smooth);
say(caption, "A forward pass turns the input into three probabilities.", 0.40);
}
}
wait(0.55);
step("measure-the-mistake") {
par {
loss(model, "1 0 0", crossentropy, 1.50, smooth);
say(caption, "The correct answer is class 1. Cross-entropy measures the mismatch.", 0.40);
}
}
wait(0.55);
step("send-credit-backward") {
par {
backward(model, 3.20, smooth);
say(caption, "The gradient carries responsibility backward through the same connections.", 0.40);
seq {
par { cam((w*0.76, h*0.47), 0.40, smooth); zoom(1.08, 0.40, smooth); }
wait(0.30);
cam((cx, h*0.47), 0.55, smooth);
wait(0.25);
cam((w*0.24, h*0.47), 0.55, smooth);
wait(0.25);
par { cam((cx, cy), 0.45, smooth); zoom(1.0, 0.45, smooth); }
}
}
}
wait(0.55);
// A zero-duration authored checkpoint captures the exact pre-update weights,
// prediction, target, and loss. It does not add a hidden runtime state.
checkpoint(beforeUpdate, model);
step("learn") {
par {
update(model, 0.18, 2.30, smooth);
say(caption, "Each parameter moves opposite its gradient; the same input is computed again.", 0.40);
}
}
wait(0.70);
step("unlearn") {
par {
restore(model, beforeUpdate, 2.30, smooth);
say(caption, "Exact rollback restores the saved parameters and their earlier prediction.", 0.40);
seq {
par { cam((w*0.24, h*0.47), 0.40, smooth); zoom(1.08, 0.40, smooth); }
wait(0.30);
cam((cx, h*0.47), 0.55, smooth);
wait(0.25);
cam((w*0.76, h*0.47), 0.55, smooth);
wait(0.25);
par { cam((cx, cy), 0.45, smooth); zoom(1.0, 0.45, smooth); }
}
}
}
wait(0.70);
step("takeaway") {
pulse(model.output, 0.75);
say(caption, "This undoes one saved update. Dataset-level unlearning is a different process.", 0.45);
}
wait(1.50);
The ML3 story turns a small image into an edge-response feature map and then a pooled summary, with the same scanner serving both operators:
// manic-ml-cnn-edge-story.manic — ML3 creator proof
// A tiny image becomes a feature map and then a pooled summary. Every number is
// computed by Manic; scan coordinates the receptive field and destination.
title("Manic ML — How a CNN Finds an Edge");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=See_the_feature safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
let cell = 48*u;
if h > 1.45*w {
watermark(manicMark, (w*0.13, h*0.075), "Made With Manic");
text(kicker, (cx, h*0.08), "MANIC ML · CONVOLUTION");
text(headline, (cx, h*0.13), "How does a CNN see edges?");
text(caption, (cx, h*0.72), "Start with a tiny image: bright cells form a simple shape.");
tensor(image, (w*0.28, h*0.35), "0 0 0 0 0; 0 0 1 1 0; 0 1 1 1 0; 0 0 1 1 0; 0 0 0 0 0", cell, cyan);
kernel(edge, (w*0.72, h*0.35), "-1 0 1; -2 0 2; -1 0 1", cell, magenta);
convolve(feature, image, edge, (w*0.34, h*0.54), 1, 0, 0, relu, cell);
pool(pooled, feature, (w*0.72, h*0.54), max, 2, 1, 0, cell);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.18, h*0.10), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · CONVOLUTION");
text(headline, (cx, h*0.14), "How does a CNN see edges?");
text(caption, (cx, h*0.82), "Start with a tiny image: bright cells form a simple shape.");
tensor(image, (w*0.17, h*0.47), "0 0 0 0 0; 0 0 1 1 0; 0 1 1 1 0; 0 0 1 1 0; 0 0 0 0 0", cell, cyan);
kernel(edge, (w*0.42, h*0.47), "-1 0 1; -2 0 2; -1 0 1", cell, magenta);
convolve(feature, image, edge, (w*0.68, h*0.47), 1, 0, 0, relu, cell);
pool(pooled, feature, (w*0.89, h*0.47), max, 2, 1, 0, cell);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · CONVOLUTION");
text(headline, (cx, h*0.13), "How does a CNN see edges?");
text(caption, (cx, h*0.82), "Start with a tiny image: bright cells form a simple shape.");
tensor(image, (w*0.18, h*0.44), "0 0 0 0 0; 0 0 1 1 0; 0 1 1 1 0; 0 0 1 1 0; 0 0 0 0 0", cell, cyan);
kernel(edge, (w*0.45, h*0.44), "-1 0 1; -2 0 2; -1 0 1", cell, magenta);
convolve(feature, image, edge, (w*0.70, h*0.44), 1, 0, 0, relu, cell);
pool(pooled, feature, (w*0.89, h*0.44), max, 2, 1, 0, cell);
}
size(kicker, 20*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 35*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 22*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
hidden(image); hidden(edge); hidden(feature); hidden(pooled);
step("pixels") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(image, 0.65);
}
wait(0.55);
step("edge-detector") {
show(edge, 0.55);
say(caption, "This 3×3 kernel compares the left side of each patch with the right.", 0.40);
}
wait(0.55);
step("feature-map") {
show(feature, 0.50);
say(caption, "The feature map begins quiet; each destination waits for one receptive field.", 0.40);
}
wait(0.35);
step("convolution-scan") {
par {
scan(feature, 4.80, smooth);
say(caption, "The same kernel slides, multiplies, sums, applies ReLU, and writes one cell.", 0.40);
seq {
par { cam((w*0.38, h*0.45), 0.75, smooth); zoom(1.10, 0.75, smooth); }
wait(2.95);
par { cam((cx, cy), 0.85, smooth); zoom(1.0, 0.85, smooth); }
}
}
}
wait(0.60);
step("pooling-map") {
show(pooled, 0.50);
say(caption, "Max pooling asks a simpler question: where is the strongest local evidence?", 0.40);
}
wait(0.40);
step("pooling-scan") {
par {
scan(pooled, 3.40, smooth);
say(caption, "Each 2×2 window keeps its first maximum; ties are deterministic.", 0.40);
}
}
wait(0.70);
step("takeaway") {
pulse(pooled.cells, 0.75);
say(caption, "A CNN builds meaning locally: pixels → feature responses → compact evidence.", 0.45);
}
wait(1.50);
The ML5 story begins with a repeated word, proves that its base lookup vector is reused, and then shows how sinusoidal position makes each occurrence unique:
// manic-ml-token-embedding.manic — ML5 acceptance story
// A repeated word keeps one seeded educational token embedding, while exact
// sinusoidal position makes each occurrence a different model input.
title("Manic ML — From Words to Positioned Embeddings");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Follow_the_representation safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.16, h*0.055), "Made With Manic");
text(kicker, (cx, h*0.055), "MANIC ML · EMBEDDINGS");
text(headline, (cx, h*0.095), "A word needs meaning—and a place");
text(caption, (cx, h*0.84), "Start with text. Keep every boundary and every number inspectable.");
tokenize(words, (cx, h*0.19), "the cat chased the cat", word, w*0.80);
embedding(context, words, (cx, h*0.50), "seeded 6 37", sinusoidal, w*0.90, h*0.48);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.16, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · EMBEDDINGS");
text(headline, (cx, h*0.125), "A word needs meaning—and a place");
text(caption, (cx, h*0.84), "Start with text. Keep every boundary and every number inspectable.");
tokenize(words, (cx, h*0.23), "the cat chased the cat", word, w*0.68);
embedding(context, words, (cx, h*0.54), "seeded 6 37", sinusoidal, w*0.90, h*0.48);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · EMBEDDINGS");
text(headline, (cx, h*0.17), "A word needs meaning—and a place");
text(caption, (cx, h*0.83), "Start with text. Keep every boundary and every number inspectable.");
tokenize(words, (cx, h*0.30), "the cat chased the cat", word, w*0.76);
embedding(context, words, (cx, h*0.60), "seeded 6 37", sinusoidal, w*0.90, h*0.38);
}
size(kicker, 18*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 32*u); bold(headline); wrap(headline, w*0.84); hidden(headline);
size(caption, 20*u); color(caption, dim); wrap(caption, w*0.82); hidden(caption);
hidden(words);
hidden(context);
step("sentence") {
show(kicker, 0.30);
show(headline, 0.42);
show(words.labels, 0.35);
show(words.source, 0.40);
show(caption, 0.35);
}
wait(0.55);
step("tokens") {
par {
show(words.tokens, 0.75);
show(words.indices, 0.75);
say(caption, "Word tokenization turns the sentence into five ordered identities.", 0.38);
}
}
wait(0.60);
step("lookup") {
fade(words, 0.35);
par {
show(context.labels, 0.40);
show(context.tokens, 0.55);
show(context.vectors, 0.95);
say(caption, "A seeded educational lookup gives each token identity one six-number vector.", 0.40);
}
}
wait(0.60);
step("same-word") {
par {
pulse(context.row1, 0.70);
pulse(context.row4, 0.70);
say(caption, "Both copies of cat reuse the same base embedding. The lookup depends on the token—not its location.", 0.42);
}
}
wait(0.65);
step("position") {
par {
show(context.positions, 1.00);
show(context.operators, 0.45);
say(caption, "Sinusoidal position adds a deterministic coordinate for places 0 through 4.", 0.42);
}
}
wait(0.65);
step("model-input") {
par {
show(context.combined, 1.00);
say(caption, "Token vector plus position becomes the model input. The repeated word now carries two different locations.", 0.42);
}
}
wait(0.70);
step("takeaway") {
par {
pulse(context.row1, 0.72);
pulse(context.row4, 0.72);
say(caption, "Meaning says what the token is. Position says where this occurrence belongs.", 0.42);
}
}
wait(1.40);
The ML4 story keeps the token lanes visible while one query reveals Q/K/V, one truthful softmax row, its weighted value mix, the residual, and a small candidate ranking:
// manic-ml-transformer-attention.manic — ML4 acceptance story
// Explicit token embeddings become Q/K/V, one query row becomes normalized
// attention weights, values mix, a residual is added, and a real output
// projection produces top-k probabilities.
title("Manic ML — One Transformer Attention Head");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Follow_the_attention safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · SELF-ATTENTION");
text(headline, (cx, h*0.105), "One token finds context");
text(caption, (cx, h*0.84), "Start with explicit token embeddings—not decorative wires.");
attention(head, (cx, h*0.38), "Art | ificial | intelligence | transforms | business",
"1 0.2 -0.4 0.7; 0.8 0.1 -0.3 0.6; -0.2 1 0.5 0.3; 0.1 0.6 0.9 -0.2; 0.7 -0.1 0.4 1",
w*0.82, h*0.40, 23);
topk(next, head, 3, (cx, h*0.71), "business | work | world | industry | future | people", 4, w*0.72, h*0.18, 29);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · SELF-ATTENTION");
text(headline, (cx, h*0.14), "One token finds context");
text(caption, (cx, h*0.83), "Start with explicit token embeddings—not decorative wires.");
attention(head, (w*0.40, h*0.48), "Art | ificial | intelligence | transforms | business",
"1 0.2 -0.4 0.7; 0.8 0.1 -0.3 0.6; -0.2 1 0.5 0.3; 0.1 0.6 0.9 -0.2; 0.7 -0.1 0.4 1",
w*0.68, h*0.58, 23);
topk(next, head, 3, (w*0.84, h*0.52), "business | work | world | industry | future | people", 4, w*0.26, h*0.34, 29);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · SELF-ATTENTION");
text(headline, (cx, h*0.13), "One token finds context");
text(caption, (cx, h*0.84), "Start with explicit token embeddings—not decorative wires.");
attention(head, (cx, h*0.39), "Art | ificial | intelligence | transforms | business",
"1 0.2 -0.4 0.7; 0.8 0.1 -0.3 0.6; -0.2 1 0.5 0.3; 0.1 0.6 0.9 -0.2; 0.7 -0.1 0.4 1",
w*0.84, h*0.42, 23);
topk(next, head, 3, (cx, h*0.72), "business | work | world | industry | future | people", 4, w*0.72, h*0.18, 29);
}
size(kicker, 19*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 34*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 21*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
hidden(head); hidden(next);
step("tokens") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(head.labels, 0.40);
show(head.tokens, 0.60);
}
wait(0.55);
step("projections") {
par {
show(head.q, 0.55);
show(head.k, 0.55);
show(head.v, 0.55);
say(caption, "Each embedding is projected into a Query, Key, and Value vector.", 0.40);
}
}
wait(0.60);
step("scores") {
show(head.matrix, 0.70);
say(caption, "Scaled dot products become one softmax row: every weight is positive and the row sums to 100%.", 0.45);
}
wait(0.65);
step("attend") {
par {
attend(head, 3, 5.20, smooth);
say(caption, "Focus intelligence: Q asks, K measures relevance, and weighted V carries the context.", 0.45);
seq {
par { cam((cx, h*0.46), 0.75, smooth); zoom(1.07, 0.75, smooth); }
wait(3.45);
par { cam((cx, cy), 0.85, smooth); zoom(1.0, 0.85, smooth); }
}
}
}
wait(0.75);
step("prediction") {
show(next, 0.70);
say(caption, "The residual plus attention mix enters an output projection and a truthful softmax ranking.", 0.45);
}
wait(0.80);
step("takeaway") {
pulse(next.rank0, 0.75);
say(caption, "Attention is selective information flow—not every connection shouting at once.", 0.45);
}
wait(1.50);
The ML6 story preserves one token lane through two causal heads, concatenation, both residual/norm stages, a GELU MLP, and the exact settled block output:
// manic-ml-transformer-block.manic — ML6 acceptance story
// One persistent token lane passes through multi-head causal attention,
// concatenation, two residual paths, pre-normalization, and a GELU MLP.
title("Manic ML — Inside One Transformer Block");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Follow_the_computation safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · TRANSFORMER BLOCK");
text(headline, (cx, h*0.105), "Inside one transformer block");
text(caption, (cx, h*0.84), "Follow one token lane through every computed stage.");
tokenize(words, (cx, h*0.15), "the cat slept because it dreamed", word, w*0.82);
embedding(context, words, (cx, h*0.41), "seeded 6 37", sinusoidal, w*0.90, h*0.38);
transformer(block, context, (cx, h*0.49), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.56);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · TRANSFORMER BLOCK");
text(headline, (cx, h*0.14), "Inside one transformer block");
text(caption, (cx, h*0.82), "Follow one token lane through every computed stage.");
tokenize(words, (cx, h*0.22), "the cat slept because it dreamed", word, w*0.72);
embedding(context, words, (cx, h*0.54), "seeded 6 37", sinusoidal, w*0.90, h*0.48);
transformer(block, context, (cx, h*0.52), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.92, h*0.62);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · TRANSFORMER BLOCK");
text(headline, (cx, h*0.13), "Inside one transformer block");
text(caption, (cx, h*0.83), "Follow one token lane through every computed stage.");
tokenize(words, (cx, h*0.23), "the cat slept because it dreamed", word, w*0.78);
embedding(context, words, (cx, h*0.52), "seeded 6 37", sinusoidal, w*0.90, h*0.44);
transformer(block, context, (cx, h*0.52), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.57);
}
size(kicker, 18*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 30*u); bold(headline); wrap(headline, w*0.86); hidden(headline);
size(caption, 19*u); color(caption, dim); wrap(caption, w*0.84); hidden(caption);
hidden(words);
hidden(context);
hidden(block);
step("tokens") {
par {
show(kicker, 0.28);
show(headline, 0.42);
show(words.labels, 0.30);
show(words.source, 0.36);
show(words.tokens, 0.70);
show(words.indices, 0.70);
show(caption, 0.32);
say(caption, "Six ordered tokens begin as one persistent lane.", 0.36);
}
}
wait(0.55);
step("model-input") {
seq {
fade(words, 0.32);
par {
show(context.labels, 0.32);
show(context.tokens, 0.42);
show(context.vectors, 0.82);
say(caption, "Each identity receives a stable lookup vector.", 0.36);
}
par {
show(context.positions, 0.72);
show(context.operators, 0.34);
show(context.combined, 0.78);
say(caption, "Position is added exactly. These six vectors are the block input.", 0.38);
}
}
}
wait(0.60);
step("encode") {
seq {
par {
fade(context, 0.34);
fade(caption, 0.24);
}
par {
encode(block, 6.2, smooth);
seq {
par { cam((cx - w*0.25, h*0.52), 0.75, smooth); zoom(1.08, 0.75, smooth); }
wait(1.35);
cam((cx + w*0.17, h*0.52), 1.15, smooth);
wait(1.50);
par { cam((cx, cy), 0.85, smooth); zoom(1.0, 0.85, smooth); }
}
}
}
}
wait(0.70);
step("takeaway") {
seq {
show(caption, 0.20);
par {
pulse(block.residual1, 0.72);
pulse(block.residual2, 0.72);
pulse(block.output, 0.72);
say(caption, "Attention shares context. The MLP reshapes each token. Residuals keep its identity continuous.", 0.42);
}
}
}
wait(1.35);
The ML7 story compares the same projection at cool and warm temperatures, then shows greedy and top-p decoding as exact filtered distributions and one seeded next-token choice:
// manic-ml-logits-sampling.manic — ML7 acceptance story
// The same LM projection is viewed at two temperatures, then top-p sampling
// filters, renormalizes, and makes one reproducible next-token choice.
title("Manic ML — How a Transformer Chooses the Next Token");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Follow_the_computation safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · LOGITS → PROBABILITIES → TOKEN");
text(headline, (cx, h*0.115), "How does a transformer choose its next word?");
text(caption, (cx, h*0.83), "One hidden vector. One full distribution. One reproducible choice.");
tokenize(words, (cx, h*0.17), "the model learned to", word, w*0.78);
embedding(context, words, (cx, h*0.42), "seeded 6 37", sinusoidal, w*0.88, h*0.34);
transformer(block, context, (cx, h*0.47), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.50);
logits(cool, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 0.55, w*0.86, h*0.54, 73);
logits(warm, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 1.45, w*0.86, h*0.54, 73);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · LOGITS → PROBABILITIES → TOKEN");
text(headline, (cx, h*0.13), "How does a transformer choose its next word?");
text(caption, (cx, h*0.82), "One hidden vector. One full distribution. One reproducible choice.");
tokenize(words, (cx, h*0.21), "the model learned to", word, w*0.64);
embedding(context, words, (cx, h*0.50), "seeded 6 37", sinusoidal, w*0.88, h*0.42);
transformer(block, context, (cx, h*0.50), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.58);
logits(cool, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 0.55, w*0.78, h*0.54, 73);
logits(warm, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 1.45, w*0.78, h*0.54, 73);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · LOGITS → PROBABILITIES → TOKEN");
text(headline, (cx, h*0.155), "How does a transformer choose its next word?");
text(caption, (cx, h*0.82), "One hidden vector. One full distribution. One reproducible choice.");
tokenize(words, (cx, h*0.21), "the model learned to", word, w*0.70);
embedding(context, words, (cx, h*0.50), "seeded 6 37", sinusoidal, w*0.88, h*0.40);
transformer(block, context, (cx, h*0.50), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.55);
logits(cool, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 0.55, w*0.82, h*0.54, 73);
logits(warm, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 1.45, w*0.82, h*0.54, 73);
}
size(kicker, 17*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 29*u); bold(headline); wrap(headline, w*0.88); hidden(headline);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86); hidden(caption);
hidden(words);
hidden(context);
hidden(block);
hidden(cool);
hidden(warm);
step("prompt") {
par {
show(kicker, 0.28);
show(headline, 0.42);
show(words.labels, 0.28);
show(words.source, 0.34);
show(words.tokens, 0.62);
show(words.indices, 0.62);
show(caption, 0.28);
say(caption, "The prompt becomes an ordered token lane.", 0.34);
}
}
wait(0.45);
step("hidden-state") {
seq {
fade(words, 0.28);
par {
show(block.output, 0.70);
show(block.labels, 0.26);
say(caption, "The transformer MLP ends at a hidden representation—not probabilities.", 0.40);
}
pulse(block.output, 0.72);
}
}
wait(0.45);
step("cool-temperature") {
seq {
fade(block, 0.30);
sample(cool, "greedy", 3.4, smooth);
say(caption, "Low temperature sharpens every candidate. Greedy keeps only the maximum.", 0.42);
}
}
wait(0.65);
step("warm-temperature") {
seq {
fade(cool, 0.30);
sample(warm, "top-p 0.90 seed=17", 3.8, smooth);
say(caption, "Higher temperature spreads the full softmax. Top-p keeps the smallest 90% nucleus, renormalizes it, then samples.", 0.46);
}
}
wait(0.75);
step("takeaway") {
par {
pulse(warm.temperature, 0.70);
pulse(warm.probabilities, 0.70);
say(caption, "Temperature reshapes probability. Sampling turns that distribution into one reproducible next token.", 0.44);
}
}
wait(1.30);
Race charts — paste a table, get a race
The charts kit turns a table into an animated race chart — ranked bars, columns, or lines that reorder over time as the numbers change. It’s the viral data-viz format (GDP, population, brands, sports…), but true to the data and self-contained in one script: no timeline scrubbing, no keyframes.
Three ideas:
- Declare a chart —
racechart(id, layout, periods) - Paste the data —
racedata(id, "…") - Play it —
race(id, seconds)
canvas("16:9"); template("blank");
racechart(gdp, "bar", "2000 2010 2020 2024", "GDP by Country ($T)");
racedata(gdp, "
USA, us, 10.3, 15.0, 21.4, 27.4
China, cn, 1.2, 6.1, 14.7, 20.5
Japan, jp, 4.9, 5.8, 5.0, 4.2
India, in, 0.5, 1.7, 2.7, 3.9
");
race(gdp, 8);
The bars grow, reorder as ranks change (the reorder is the race), the axis rescales with the running max, and the year + values tick — all from that block.
The data block
racedata(parent, "block") is the heart of it. One row per entity:
label [icon] v0 v1 v2 … ← one value per period
The parser is forgiving so real data pastes straight in:
- Rows split on a newline or a
;. - Cells split on comma, tab, or whitespace — so a CSV, a TSV, or a spreadsheet copy all work.
- Missing cells count as 0 (e.g. a company/party that didn’t exist yet).
- Multi-word labels are fine when the data is comma- or tab-delimited.
Prefer to build rows in code? raceseries adds one entity at a time — the
computed/loop path:
for i in 0..n {
raceseries(g, name{i}, "rocket", vals{i}); // label, icon, values
}
Icons — any SVG on the bar
The optional icon column puts a flag, logo, or emoji on each bar. It takes any SVG, not just flags:
| You write | Resolves to |
|---|---|
us | a flag shorthand → asset:svg/flags/us.svg |
rocket, heart, trophy | an emoji / icon alias |
asset:svg/lucide/lightbulb.svg | an explicit set path |
logos/acme.svg | your own file (any path ending .svg) |
| (omit it) | the row is just label + values |
Icons are imported as real geometry via svg(), so they recolour
and scale cleanly. Browse every bundled asset in the SVG catalogue.
A handful of flags whose stars use SVG
<use>/<marker>(e.g. China) render imperfectly for now — most flags and all icons/emoji are clean.
Layouts
racechart(id, layout, …) takes one of three:
"bar"— horizontal bars, ranked top-to-bottom. The classic; gets value gridlines + a live Total readout."column"— vertical bars, ranked left-to-right."line"— each series a line that draws on over time; the race is which line climbs highest (fixed axis, tip labels auto-declutter).
Bar + line, together
On a bar race you can overlay a companion line:
raceline(parent)— a line across the top showing a running total (auto-summed from the bars), orraceline(parent, "label", "v0 v1 …")for an explicit metric.racepanel(parent)— a multi-line history panel below the bars: every series as a line revealed up to a moving time-cursor (the Flourish bar+line combo).
racechart(de, "bar", "1949 … 2021", "German Elections %");
racedata(de, " CDU/CSU, , 31, … ; SPD, , 29, … ; … ");
racepanel(de); // history panel below the bars
race(de, 16);
Composing with the rest of manic
A race chart is just tagged entities, so it drops into any scene — a voiced
Creator Short, a quiz reveal, a caption sequence. In
creator-race-quiz a quiz poses a question and
the race chart proves the answer, narrated with speak, closed with a CTA
card — the chart running inside a par while the voice-over plays over it.
Examples
- GDP bar race — flags racing, gridlines, total
- Column race — tennis Grand Slams
- Line race — GDP trajectories
- Bar + total line
- Bar + history panel — from a real CSV
- Race-chart quiz Short — voiced, problem→proof→CTA
Chemistry — molecules that know their own shape
The chemistry kit loads a real molecule from a real file. molecule3 reads an
MDL structure file — an .sdf from PubChem’s Download → SDF, or a .mol from
RDKit or Open Babel — and lays out its atoms and bonds as ordinary 3-D entities.
The point is that nothing about the shape is authored. The coordinates are the file’s, in Ångströms, so a bond angle on screen is the angle in the conformer someone computed. Hand-placing atoms gets a molecule that looks plausible; this gets one that is right.
canvas("16:9"); template("black");
camera3((0, -14, 4), (0, 0, 0), 40);
molecule3(caf, "asset:molecules/caffeine.sdf");
wait(4);
Three lines, and caffeine is on screen with its real geometry, turning slowly — because a shape only reads in three dimensions. Flat, methane is four lines from a dot.
The options
Everything past the path is optional:
molecule3(id, path, [center], [scale], [spec])
center— where to put it, default the origin. The molecule is centred on its own centroid first, so it lands in front of the camera whatever the file’s coordinates happened to be.scale— world units per Ångström, default 1.5. A small molecule is then a few units across, which is the scale the rest of the 3-D kit works at.spec— a boundedkey=valuestring, below.
| key | values | meaning |
|---|---|---|
style | ball (default) · sticks · wireframe · spacefill | how it is drawn |
hydrogens | 1 (default) · 0 | 0 drops them and their bonds |
spin | degrees per second, default 24 | 0 holds it still |
axis | z (default) · x · y | which axis the turn is about |
turn / tilt | degrees | a static orientation, applied once |
record | 1 (default) | which molecule of a multi-record SDF |
molecule3(caf, "asset:molecules/caffeine.sdf", (0,0,0), 0.9, "style=sticks hydrogens=0");
molecule3(bz, "asset:molecules/benzene.sdf", (0,0,0), 1.2, "spin=0 tilt=90");
molecule3(eth, "asset:molecules/alkanes.sdf", (0,0,0), 2.0, "record=2");
The four styles answer four questions
- ball & stick — what is bonded to what, and the shape. The textbook default.
- sticks — the same connectivity with the spheres out of the way of the geometry. Bonds are two-tone, split at the midpoint and coloured by the atom each half touches, so a bond still says what it joins.
- wireframe — thin lines only, for a large structure where anything solid hides it.
- spacefill — full covalent radii, no bonds: the shape the molecule presents to the world, which is the one that decides what it fits into.
Hiding hydrogens
Half the atoms in an organic molecule are hydrogens, and often they are noise.
hydrogens=0 removes them, removes the bonds that reached them (or the molecule
sprouts sticks into empty space), and re-centres on what is left — so the molecule
does not drift off the axis it spins about.
Animation is the default
The turn is on unless you switch it off, and it costs no timeline verb: it is a
pure function of t, on the same per-frame path a cloud recomputes on.
So it scrubs backwards exactly and records identically every run.
It rotates the molecule about its own axis, not around the world origin, and it
writes the entity’s sync-owned home — which means shift3 and friends still work
on top of it. A molecule can travel and turn at once, rigidly.
Structural formulae — the flat drawing
structure is the other half of the kit: the skeletal formula a chemist puts on
paper, in 2-D.
canvas("16:9"); template("paper");
structure(glc, "asset:molecules/glucose-2d.sdf", (400, 360), 62);
wait(3);
That draws the pyranose ring with its five OH groups, its ring oxygen lettered,
its carbons as bare vertices, and wedge and hash bonds where the file records
stereochemistry — glucose’s 2-D record carries four of them, which is the
difference between one sugar and another.
Every convention is derived from the file rather than authored:
| what you see | what justifies it |
|---|---|
| a carbon is a bare vertex | the element is C |
| hydrogens on carbon vanish | they are bonded to a C |
an oxygen reads OH | it has one explicit H neighbour |
a nitrogen reads NH2 | it has two |
| a double bond leans into the ring | the crowded side, from the neighbours’ own coordinates — no ring detection needed |
| a bond is a solid wedge | the molfile’s fourth bond column says 1 |
a lone carbon reads CH4 | a single bare vertex is not a drawing of methane |
Bonds stop short of a letter, so no line runs under text.
structure(id, source, [center], [unit], [spec])
source is either a file or a SMILES string — see
From a SMILES string below.
unit is pixels per bond (default 64) — a 2-D record is laid out on a
1.0-per-bond grid, so it means exactly what it says.
| key | values | meaning |
|---|---|---|
carbons | 0 (default) · 1 | letter every carbon too — the explicit style a first lesson uses |
hydrogens | 0 (default) · 1 | draw them as their own vertices instead of folding them in |
labels | text (default) · latex | how the letters are set |
color | any palette name or #rrggbb | the bond and carbon colour |
record | 1 (default) | which molecule of a multi-record file |
Typeset labels
labels=latex sets each label through the same LaTeX engine as equation, so a
subscript is a real subscript and a charge is raised:
structure(gly, "asset:molecules/glycine-2d.sdf", (400, 360), 74, "labels=latex");
NH2 becomes NH₂, SO4^2- becomes SO₄²⁻, and element symbols stay upright
(\mathrm{}) because an element is not a variable — italic O is wrong in a way a
chemist notices at once. Element colours carry through the tint.
The cost is one rasterised image per distinct label, not per label: glucose has
six lettered atoms but only two distinct strings (OH and O), so it costs two.
Plain text stays the default because it is free and legible at any size — reach for
LaTeX when the frame is going in front of an audience.
It needs the 2-D record
PubChem serves both, and they are not interchangeable:
…/compound/cid/5793/record/SDF?record_type=2d ← for `structure`
…/compound/cid/5793/record/SDF?record_type=3d ← for `molecule3`
The 2-D coordinates are laid out for depiction — a clean grid, regular hexagons,
nothing overlapping. A 3-D conformer flattened is a tangle of crossing bonds, so
structure refuses one and names the fix. The bundled 2-D files are
asset:molecules/<name>-2d.sdf.
From a SMILES string
A file is not always what you have. A reaction record, a paper, a database export
gives you SMILES — a molecule as a line of text — and structure takes one
directly:
structure(eth, "CCO", (300, 360), 70); // ethanol
structure(asp, "CC(=O)Oc1ccccc1C(=O)O", (800, 360), 54); // aspirin
There is no second builtin and no conversion step: a string that does not look
like a file — no asset:, no /, no .sdf or .mol — is read as SMILES. So the
two forms never collide, and a scene can mix them freely.
The difference is where the coordinates come from. A file carries its depiction; a SMILES string does not, so the layout is computed:
| what you get | the rule behind it |
|---|---|
| a chain zig-zags | each bond turns 60° from the last, so the interior angle is the tetrahedral 120° |
| a ring is a regular polygon | closed on its own circumcircle, whatever its size |
| a fused ring reflects across the shared bond | so naphthalene reads as naphthalene, not as two rings on top of each other |
| a substituent points out of its ring | radially, away from the ring centre — toluene’s methyl never points inward |
| an aromatic ring gets real double bonds | the lowercase c1ccccc1 form is Kekulé-ised across the whole aromatic system |
The layout is checked before it is drawn, and this is the whole safety argument. Overlapping atoms, crossing bonds and a stretched bond are refused — with the 2-D-file fix named — rather than drawn as a tangle:
structure(m, "CN1CC[C@]23c4c5ccc(O)c4O[C@H]2[C@@H](O)C=C[C@H]3[C@H]1C5");
error: the layout for `CN1CC[C@]23c4c5ccc(O)c4O[C@H]2[C@@H](O)C=C[C@H]3[C@H]1C5` is
not clean enough to draw: the bond between atoms 20 and 21 came out 4.00 bond-lengths
long. Laying a molecule out from SMILES is reliable for chains, single rings and
simply fused systems; a complex polycyclic needs a real depiction. Use a 2-D structure
file instead — asset:molecules/<name>-2d.sdf if it is bundled, or a PubChem download
with record_type=2d
That is morphine: five fused rings including a bridge, which is exactly what growing a layout outward cannot place. Fused systems that grow outward are fine, and that covers more than it sounds like — a steroid’s four rings, a substituted phenanthroline, a linear or angular aromatic all draw correctly. A bridged or caged skeleton does not, and says so.
The point is that nothing draws wrong. It either draws, or it tells you what to do instead — because a plausible-but-incorrect structural formula is worse than no drawing at all.
One consequence is worth knowing, because it will look like a bug otherwise: the
layout grows along the string, so two SMILES for the same molecule need not behave
the same. The 4,7-dimethoxy-1,10-phenanthroline in
examples/reaction-ord-screen.manic is the case in point:
structure(l, "COC1=CC=NC2=C3N=CC=C(OC)C3=CC=C12"); // refused — a bond 3.6 long
structure(l, "COC1=CC=NC2=C1C=CC1=C2N=CC=C1OC"); // the same molecule, draws cleanly
Where the ring closures fall decides the order the rings get placed in, and one order paints the layout into a corner. If a string you did not write is refused, a canonical form of it — from PubChem, RDKit or Open Babel — is worth trying before reaching for a file. But when the exact string matters to the scene, use the depiction file and say why; that is what the escape hatch is for.
Atoms are numbered in written order from zero, which is often the most useful
handle you could ask for. CC(=O)O writes its hydroxyl oxygen last, at a3:
structure(acid, "CC(=O)O", (190, 348), 76);
recolor(acid.a3, crimson, 0.6); // the OH that is about to leave
Only lettered atoms get a label entity, so a{i} addresses heteroatoms — which is
usually what a lesson wants to point at anyway.
See examples/reaction-esterification.manic, which builds A + B → C entirely from
SMILES and marks the atoms that become the water.
Writing it on
Because bonds are paths and labels are text, the handwriting effect is a core-kit idiom — nothing chemistry-specific:
structure(caf, "asset:molecules/caffeine-2d.sdf", (350, 372), 54);
untraced(caf.bonds);
hidden(caf.labels);
stagger(0.05) { draw(caf.bonds, 0.4); } // bond by bond
show(caf.labels, 0.6);
Address {id}.bonds, not {id}.b0…: a skeletal drawing skips every C–H bond, so
the surviving ids keep the file’s numbering with gaps in it.
And since these are 2-D entities, framebox and brace work on them — which they
do not on molecule3.
A molecule that couldn’t exist is refused
structure(x, "C(C)(C)(C)(C)C");
error: `C(C)(C)(C)(C)C`: this is not a molecule that can exist: atom 1 is C with
5 bonds, but C takes 4. If it is meant to be an ion, say so — `[C+]` or `[C-]` —
since a charge changes what a valence is allowed to be.
This is a separate check from the layout one, and it has to be. That carbon with five bonds lays out perfectly cleanly — no crossings, no overlaps, every bond the right length — so the geometry gate passes it and draws a confident picture of something impossible. Asking “is this readable?” and asking “is there anything to draw?” are different questions.
The rule is bond-order sum against the element’s allowed valences, adjusted for formal charge, which is what keeps it useful rather than pedantic:
structure(a, "O(C)(C)C"); // refused — three bonds on a neutral oxygen
structure(b, "[N+](C)(C)(C)C"); // fine — that is an ammonium ion
It runs on files too, not only SMILES strings — a hand-edited or carelessly converted file is exactly where a bad valence turns up.
What it does not catch, stated plainly: it is a capacity check, asking whether the atom could accommodate that many bonds. Nitrogen’s allowed valences are 3 and 5, so a neutral four-bonded nitrogen slips through — 5 could hold it, even though such a nitrogen is really an ammonium missing its charge.
The stricter alternative — requiring the count to equal an allowed valence — was measured against the bundled library and rejected. It refuses nitric oxide (a genuine radical, and a molecule any biology lesson wants), ferrocene and cyanocobalamin. Rejecting those to catch a four-bonded nitrogen is a bad trade, so the looser rule stands. Hypervalent sulfur and phosphorus, metal complexes and aromatics all pass either way.
What it’s called
A structure carries its own names, as two captions under the drawing:
structure(caf, "asset:molecules/caffeine-2d.sdf", (400, 300), 56);
show(caf.name, 0.4); // caffeine
show(caf.iupac, 0.4); // 1,3,7-trimethylpurine-2,6-dione
The gap between those two lines is the nomenclature lesson. Reveal the word, then reveal the systematic name, and the point makes itself — no narration needed.
Both start hidden, so show is the entire opt-in and no existing scene changes.
The two names come from different places, and the difference is worth understanding:
| source | available for | |
|---|---|---|
{id}.iupac | the record’s own name, or generated | bundled files, your files, and SMILES within a narrow scope |
{id}.name | the curated asset:molecules/ filename | bundled molecules only |
The systematic name is read straight from the record, which makes it authoritative and
complete — stereodescriptors included, since that is most of what makes a real name
hard. PubChem writes those with markup ((8<I>R</I>,9<I>S</I>)-…), so the tags are
stripped before anything reaches the screen. For a SMILES string there is no record, so
the name is generated; outside a narrow scope the generator declines, and then there is
simply no iupac entity rather than a guess.
The everyday name is stranger, and worth explaining because the obvious approach fails.
Nothing inside a record carries it. The title line is the bare compound id, and
PubChem’s PUBCHEM_IUPAC_TRADITIONAL_NAME field — which looks exactly like the field
for it — is semi-systematic as often as not:
caffeine -> caffeine ✓
dopamine -> 4-(2-aminoethyl)pyrocatechol ✗
serotonin -> 3-(2-aminoethyl)-1H-indol-5-ol ✗
testosterone -> (8R,9S,10R,13S,14S,17S)-17-hyd… ✗
No length or shape rule separates the two, and a common name is a fact about usage
rather than structure, so it cannot be computed at all. The one place Manic genuinely
holds it is the library’s own filenames — every file under asset:molecules/ was
fetched by resolving a common name to a compound id, so the slug is the name. That is
why {id}.name exists for asset:molecules/dopamine-2d.sdf and not for your own file:
anyone’s thing-2d.sdf could be called anything.
Long names wrap rather than running off the canvas — 25 of the bundled molecules have systematic names over 100 characters, and oxytocin’s is 306.
Functional groups are addressable
A structure is also tagged by the functional groups in it, perceived from the structure itself:
structure(asp, "CC(=O)Oc1ccccc1C(=O)O", (400, 360), 62);
color(asp.carboxyl, crimson); // the acid: C, =O, OH, and the bonds between them
color(asp.ester, indigo); // the acetyl group, including its linking O
color(asp.aromatic, crimson); // the whole benzene ring
This is the same mechanism as the element tags ({id}.O, {id}.N) pointed at a
different fact about an atom — so every core verb already works on it. Nothing
chemistry-specific was added to make pulse(glc.hydroxyl) or
framebox(box, asp.aromatic) work.
The twenty groups:
hydroxyl | carbonyl | carboxyl | aldehyde |
ketone | ester | amide | primary_amine |
secondary_amine | tertiary_amine | nitro | nitrile |
thiol | sulfide | sulfoxide | sulfone |
halogen | ether | epoxide | aromatic |
Four things worth knowing about how they behave:
A tag covers the bonds, not only the letters. Colouring a carboxyl that left its C=O line black would not be what you asked for, so a bond with both ends inside the group carries the tag too.
Every repeat shares one tag. Open-chain glucose has five hydroxyls, and
color(glc.hydroxyl, indigo) colours all five — which is what the singular reads as.
Groups overlap on purpose. Acetic acid reports carboxyl, and also carbonyl and
hydroxyl, because each of those is true of it and a lesson might point at any one.
Deciding a carboxyl is “not really” a carbonyl would be the engine inventing chemistry.
An ester, on the other hand, reports ester and not carboxyl — there is no free
acid in it.
A file and a string give the same answer. That takes a little work behind the
scenes: PubChem writes benzene in Kekulé form, with alternating single and double
bonds and no aromatic flag anywhere, so aromaticity is perceived (Hückel) rather than
taken on trust. Without that, asp.aromatic would find a ring in
CC(=O)Oc1ccccc1C(=O)O and miss the identical ring in aspirin-2d.sdf.
A group that is not present is simply not tagged, and addressing it is an error that
names the id — butane.hydroxyl does not silently match nothing.
Addressing the chemistry
Every piece is a plain tagged entity, so the core kit does the presentation:
| address | what it is |
|---|---|
{id}.a{i} | the i-th atom, in file order |
{id}.b{k} | the k-th bond (.{n} per line of a multiple bond, .h{0,1} per half) |
{id}.atoms · {id}.bonds | the two groups |
{id}.multibonds | only the double and triple bonds |
{id}.<element> | every atom of one element — {id}.O, {id}.N, {id}.Fe |
That last one is the useful one, because it is how a lesson actually talks:
color(caf.N, cyan); // "the nitrogens"
pulse(caf.O); // "watch the oxygens"
view3(caf.N, "front"); // fly the camera to frame them
opacity(caf.H, 0.25); // push the hydrogens back
show, fade, hidden, color, hue, opacity, pulse, to, draw +
untraced, shift3, rotate3, orbit3, camera3, finish3 and view3 all
work on a molecule, a group or one element.
Two limits worth knowing. framebox and brace measure 2-D screen bounds and
do not see 3-D entities at all — that is true of model3 and sphere3 as much as
a molecule; view3 is the 3-D framing verb. And draw traces the bonds on (a
bond is a path); an atom is a sphere, so it swells rather than draws.
The bundled molecules
192 molecules, 392 files under asset:molecules/ — byte-exact PubChem
downloads, most with both a 3-D conformer (<name>.sdf) and a 2-D depiction
(<name>-2d.sdf). Enough to teach from without downloading anything.
Grouped the way a syllabus is: inorganics and gases · hydrocarbons · aromatics and heterocycles · alcohols, carbonyls, acids and solvents · sugars · all twenty amino acids · nucleobases and nucleotides · vitamins · lipids, steroids and hormones · neurotransmitters · medicines · flavours and famous molecules · industrial monomers.
The full table with compound ids is
assets/molecules/INDEX.md.
Some have no 3-D conformer — mostly salts, metal complexes and the largest
molecules — so only the -2d file exists. And three names are deliberately absent:
cellulose, starch and haemoglobin are polymers or proteins rather than compounds, so
PubChem has no single record for them.
Three honest caveats:
- They are computed conformers, not measured structures. PubChem optimises with MMFF94, so a bond length sits within about 0.05 Å of the experimental value rather than on it. Right for teaching shape; not a source for a number you intend to quote.
- Some have no 3-D conformer at all — mostly the inorganics and salts — so the 2-D depiction is bundled and every z is zero. Such a molecule lies flat, and tilting it will show it edge-on.
- PubChem writes benzene in Kekulé form, alternating single and double, rather than as a delocalised ring. That is the file being explicit, not the reader guessing.
Your own file
Any .sdf or .mol works — pass a path instead of an asset: URI. From PubChem:
https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/2519/record/SDF?record_type=3d
A V3000 molfile is refused with the command to convert it, rather than
mis-parsed: obabel in.sdf -O out.sdf --outputV2000.
An example end to end
Why molecules have shapes — four molecules, four geometries, and not one coordinate in the script:
canvas("16:9"); template("black");
camera3((0, -15, 8.5), (0, 0, 0), 40);
molecule3(w, "asset:molecules/water.sdf", (-6.4, 0, 0), 1.5);
molecule3(am, "asset:molecules/ammonia.sdf", (-2.1, 0, 0), 1.5);
molecule3(me, "asset:molecules/methane.sdf", ( 2.2, 0, 0), 1.5);
molecule3(co, "asset:molecules/carbon-dioxide.sdf", ( 6.6, 0, 0), 1.5);
text(t1, (196, 566), "bent"); text(t3, (760, 566), "tetrahedral");
text(t2, (452, 566), "pyramidal"); text(t4, (1060, 566), "linear");
wait(6);
The labels are shape classes, not measurements — the geometry on screen is the file’s, and there is no number in the script to disagree with it.
See the chemistry gallery for the full scenes.
Circuits — draw one, and it works
The circuit kit takes a netlist and gives you a schematic that draws itself, carries real current, and answers questions. The numbers are not painted on: every voltage, current and glow comes from a Modified Nodal Analysis solve of the circuit you typed, pre-simulated at build time so it scrubs and records exactly.
Three ideas:
- Type the circuit —
circuit(id, center, netlist) - Play it —
run(id, seconds) - Take something out —
cut(id, part)/reconnect(id, part)
canvas("16:9"); template("paper");
circuit(rc, (640, 360), `
dc-voltage 0 4 0 0 v=5
resistor 0 0 5 0 r=1k
capacitor 5 0 5 4 c=1u
wire 5 4 0 4
`);
probe(rc, (5, 0));
run(rc, 6);
That block draws a schematic one component at a time, starts the current the moment the loop closes, and reads the capacitor charging to 63.2% of the supply at exactly one time constant — because it solved an RC circuit.
The netlist
One component per line. Two grid points, then optional key=value parameters:
<type> <x1> <y1> <x2> <y2> [key=value …]
ground <x> <y>
# a comment
The topology comes from the geometry. Points that coincide — or are joined by
a wire — are the same electrical node. There are no node numbers to write and
none to get wrong:
circuit(divider, (640, 360), `
dc-voltage 0 6 0 0 v=9
resistor 0 0 4 0 r=3k # these two share the point (4,0),
resistor 4 0 4 6 r=1k # so that IS the divider's tap
wire 4 6 0 6
ground 0 6
`);
Values take SI suffixes — 1k, 4.7u, 100n, 10m, 2M. Lowercase m is
milli and M is mega (SPICE’s habit of writing mega as M is a trap, so it is
not one here).
ground is a reference, not a component
The solver needs one node to measure everything else from. With no ground line
the kit elects the first source’s negative terminal, so a single loop solves
identically either way — the symbol adds nothing and a textbook figure of a cell
and a lamp does not draw one. Write a ground when several sources or loops make
it worth saying which node is zero, or when the schematic’s convention wants the
symbol.
A three-pin part still takes two coordinates
Its extra pins are derived, and this is the one thing worth memorising:
| part | pins |
|---|---|
transistors, opamp, gates, comparator, vco, ideal-switch, spdt-switch | third pin one step below the second coordinate |
| flip-flops | two outputs, one and two steps below the second coordinate |
relay | third below the second, fourth below the first |
transmission-line, dependent sources | output pair below both written points |
circuit(amp, (640, 360), `
bjt-npn 3 3 6 3 # base (3,3) · collector (6,3) · EMITTER (6,4)
…
`);
If a pin ends up connected to nothing, the error names it and its coordinate rather than saying “the circuit will not settle”.
It draws itself
A netlist is already written in the order a teacher would draw the circuit, so that is the default: components arrive one at a time, traced on, and each prefix of the netlist is really solved.
So nothing flows while the loop is open — the charge dots do not move because
there is nothing to move them — and the instant the closing component lands,
current appears everywhere at once. A probe reads 0.00 mA and then the true
value. Nobody animates that.
The sixth argument is the share of run spent drawing:
circuit(rc, (640, 360), `…`, 46, 1, 0.35); // the default
circuit(rc, (640, 360), `…`, 46, 1, 0); // already drawn at t = 0
Use build 0 when the first frame matters — a film’s opening shot, a thumbnail —
because a self-drawing circuit is invisible until a run plays it. (The
editor warns if you forget the run.)
Reading it: probes and scopes
probe puts a live number on the schematic. Point it at a grid point for that
node’s voltage, or name a part for its current:
circuit(rc, (560, 340), `
dc-voltage 0 4 0 0 v=5
resistor 0 0 5 0 r=1k name=R1
capacitor 5 0 5 4 c=1u
wire 5 4 0 4
ground 0 4
`);
probe(rc, (5, 0)); // volts at the capacitor's top
probe(rc, R1, (80, -20)); // amps through R1, label nudged clear
scope(rc, (5, 0), (1020, 300), 380, 150);
run(rc, 6);
A scope traces the pre-simulated waveform with a sweep line that keeps step
with run, and labels both axes with their real extent. Its window belongs to
the circuit it was declared against — after a cut, declare another one or use a
probe, which does follow across.
The current you can see
Charge dots ride every branch. Their position is ∫I dt — accumulated charge
— from the same branch currents the solver produced, so a branch carrying twice
the current always moves twice as fast, and a dot and a probe can never disagree.
current is the animator’s dial. It changes nothing about the physics:
current(rc, 2); // twice the pace
current(rc, 1.4, diamond, crimson, 4); // speed, shape, colour, size
speedmultiplies every branch by the same factor, so the proportionality survives — 1 is the circuit’s own pace, 2 is a fast cut, 0.5 is one you can talk over.shapeiscircle(default),squareordiamond.colortakes any palette name or#rrggbb. The default is gold, the conventional-current colour; cyan is the usual choice for electron flow.
A branch with no current shows no dots at all, rather than a frozen row of them — a stalled queue of dots reads as current, which is the one misreading worth designing against.
A lamp glows because it is dissipating power
lamp and led carry a halo whose opacity is P / P_ref, with P = V·I from
the same series a probe reads. Two lamps in series share one current, so they
light equally; a 300 Ω lamp in series with a 100 Ω one comes out three times
brighter, because at one current P = I²R.
The reference is fixed by the circuit as first built, so brightness is comparable
across the schematic and a cut genuinely darkens it. The halo is an ordinary
entity tagged {id}.glow:
color(fig.glow, orange); // a warm glow reads as light on `paper`
Take something out, and mean it
cut removes a component from the circuit, not from the picture: what is left
is re-analysed and re-solved from scratch. Break a series loop and the current
stops everywhere, and a meter on another branch falls to zero on its own.
run(fig, 6); // both lamps lit
cut(fig, L1, 0.8); // one filament breaks
run(fig, 3); // … and neither glows
reconnect(fig, L1); // mend it
run(fig, 3); // both back
Address the part by its netlist name=, or by position as c0, c1, … Each
run after a change replays the circuit that now exists. A cut that leaves
something unsolvable (cutting the only source) is refused, with the part named.
cut decides the physics; you decide the theatre. By default the component
fades out with its label, every dot and glow goes with it, and each probe moves to
what it now reads. If the story is a broken filament rather than a lamp taken
out of its holder, bring the symbol back and mark the break:
cut(fig, L1, 1.0);
par {
show(fig.L1, 0.4); // the lamp is still there …
show(break1, 0.35); // … it just cannot carry anything
}
Everything is an ordinary entity
The kit adds no animation vocabulary. Every piece is a normal tagged entity, so the core verbs work:
| address | what it is |
|---|---|
{id}.c{k} | the k-th component, in netlist order |
{id}.<name> | a component you gave name=X |
{id}.parts | every component (stroke batch) |
{id}.labels | the value labels |
{id}.nodes · {id}.n{node} | junction dots |
{id}.charge | the charge dots |
{id}.glow | lamp/LED halos |
{id}.probes · {id}.scopes | readouts |
{id}.<type>s | every part of one type — {id}.resistors, {id}.lamps |
framebox(ring, fig.L1, 10); // ring the component you are talking about
par { zoom(1.7, 1.2); cam((820, 420), 1.2); }
pulse(fig.L2);
color(fig.resistors, indigo);
erase(fig.parts, 0.4); // erase the STROKES …
fade(fig.labels, 0.3); // … and fade what is not a stroke
That last pair is the one gotcha: erase/draw are stroke verbs, so on a bare
circuit id they leave the labels and dots behind. Address {id}.parts and fade
the rest.
What is in the kit
44 component types, from the 102-preset library the solver was ported from:
| group | types |
|---|---|
| passive + sources | wire ground resistor capacitor polarized-cap inductor dc-voltage ac-voltage dc-current clock lamp |
| switching | switch push-switch spdt-switch ideal-switch relay fuse |
| junctions | diode led zener |
| transistors | bjt-npn bjt-pnp mosfet-n mosfet-p jfet-n jfet-p darlington-npn darlington-pnp |
| analogue | opamp comparator schmitt vco transmission-line |
| combinational logic | and-gate or-gate nand-gate nor-gate xor-gate not-gate logic-input logic-output |
| sequential logic | d-flipflop sr-flipflop jk-flipflop |
Junctions, transistors and op-amps are solved by Newton–Raphson with voltage limiting; capacitors and inductors use trapezoidal companion models; a Darlington really is two transistors sharing an internal node, and a transmission line really is a ladder of L–C sections, which is why a step arrives late.
The physics you can rely on
- KCL and KVL hold by construction. Every node row of the matrix is a current balance and every voltage source adds a row that pins its branch, so any solution the kit accepts satisfies both to solver precision.
- The timestep is the circuit’s, not the frame rate’s. It integrates at the smallest time constant the circuit demands and decimates the output down to playback. Coarsening the integration would change the answer; coarsening what is stored cannot.
- It is pure in
t. The whole transient is solved at build time, so scrubbing backwards — including back through a construction or a cut — is exact. - Closed-form checks, in the test suite. Ohm’s law, series/parallel, a
divider’s tap, an RC step at 63.2% of one time constant, RL rise, series-RLC
resonance at
1/(2π√(LC)), a diode’s forward drop, a half-wave rectifier’s floor, β² gain on a Darlington, a 1 µs delay line arriving at 1 µs.
End to end: a class-9 question
“If the filament of one of the lamps is broken, will the other glow? Justify your answer.” The answer is nowhere in this file — it comes out of the solver.
canvas("16:9"); template("paper");
circuit(fig, (560, 330), `
lamp 1 0 5 0 r=120 name=L1
lamp 5 0 9 0 r=120 name=L2
wire 9 0 9 4
wire 9 4 6 4
dc-voltage 6 4 4 4 v=6
wire 4 4 1 4
wire 1 4 1 0
ground 6 4
`, 62, 0, 0);
current(fig, 1.4, circle, crimson, 4);
color(fig.glow, orange);
probe(fig, L1, (-14, -52));
run(fig, 6); // one current, so both lamps light equally
cut(fig, L1, 1.0); // break one filament
run(fig, 4); // both dark, and the meter reads 0.00 mA
reconnect(fig, L1, 1.0);
run(fig, 4); // mended
A series circuit is one path. Break it anywhere and you have broken it everywhere — and here that sentence is a measurement, not a claim.
See the circuits gallery for the full scenes.
Diagrams — draw a system, then watch it work
Declare a technical diagram as text and manic animates it — architecture
diagrams today, with flows, sequences, and more to come. Declare what belongs
where, connect the topology, then follow one message through the design. Manic
handles nested responsive layout, provider artwork, parallel lanes, and
deterministic motion; ordinary step, show, draw, flow, camera, and text
verbs remain the presentation language.
Unlike static diagram-as-code (Mermaid, Mingrammer), a manic diagram moves: the same source that draws the boxes also carries a request through them.
The vocabulary
The whole kit is nine words — a behaviour layer over diagram structure, not a catalogue of cloud verbs:
architecture cluster node connect link
message route hotpath flow
With them you get provider-neutral nodes (no assets) or real icons from 17 providers; declaration-first nested clusters with responsive layout; direct, curved, and orthogonal connections with node-boundary ports; cold dashed topology kept separate from solid runtime motion; and one persistent message whose identity survives every hop. The kit shines for one clear story — and density is no longer your job: geometry is optional, so even a dense platform overview auto-fits and scales itself to stay in-frame (see It just fits, below).
Possible vs. actual
The one rule to internalise: a connection states what is possible; a moving message states what happened.
connect(toWorker1, queue, worker1, orthogonal, right, left);
connect(toWorker2, queue, worker2);
connect(toWorker3, queue, worker3, orthogonal, right, left);
message(job, queue, "101");
route(job, toWorker2, 0.9, linear);
route(job, worker2ToDatabase, 0.9, linear);
The three connections state what is possible. Only the two explicit route
calls state what happened. Manic does not claim RabbitMQ selected Worker 2; the
creator authored that selection.
The mental model
| Layer | Words | Meaning |
|---|---|---|
| Ownership | architecture, cluster, node | what belongs where |
| Topology | connect, link | possible directed paths and neutral visual relationships |
| Runtime story | message, route, hotpath | what one persistent message actually does |
| Aggregate activity | flow | untracked traffic over one path or a path group |
request is an HTTP-friendly alias of message. Provider services are data,
not vocabulary: use "aws:lambda", not a separate Lambda constructor.
Provider names are visual metadata only. Manic does not infer balancing,
queueing, broadcasting, retries, or any other behavior from an icon or label.
Use explicit route/travel for one authored journey, seq for authored order,
and par plus multiple objects when several journeys should happen together.
hotpath is only an optional seeded walk over declared graph geometry; it is
not a simulation of the services shown.
It just fits — no coordinates
Geometry is optional. Write architecture(id) with no center or size and the
diagram fills the canvas (leaving the title and caption bands clear), centres
itself, and lays out every cluster and node for you:
architecture(platform); // that's the whole canvas declaration
Then add as much as the story needs. Leaf clusters with many children wrap into a grid, top-level tiers wrap into rows, and when the packed content would still overflow the frame, the whole diagram scales down as one — cards, icons, labels, and every connection lane together — to a legible minimum. Nothing clips off-canvas, and you never touch a coordinate, split a cluster into columns, or shrink a font to make it fit.
This example declares Route 53, an edge load balancer, a gateway, three availability zones of services, a replicated database cluster, and monitoring — all with zero geometry. Add a tier or a zone and it simply re-fits:
title("A Microservices Platform — Laid Out Automatically");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic");
text(kicker, (cx, h*0.055), "DIAGRAMS · AUTO-FIT · NO COORDINATES");
text(headline, (cx, h*0.12), "Add a tier. It re-fits itself.");
text(caption, (cx, h*0.95), "DNS, load balancing, three AZs of services, a database cluster, and monitoring — all auto-laid.");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 26*u); bold(headline); wrap(headline, w*0.86);
size(caption, 17*u); color(caption, dim); wrap(caption, w*0.86);
// No geometry: the diagram auto-fits. Route 53, an edge load balancer, three
// availability zones, a replicated database cluster and monitoring all reflow
// to fit — and when the content would overflow, the whole diagram scales down
// as one. The author types no coordinate.
architecture(platform);
node(dns, platform, "aws:route53", "route 53");
node(lb, platform, "aws:elb", "load balancer");
node(gateway, platform, "aws:api-gateway", "gateway");
cluster(region, platform, "REGION us-east-1");
cluster(azA, region, "AZ-A");
node(a1, azA, "aws:ecs", "svc"); node(a2, azA, "aws:ecs", "svc"); node(a3, azA, "aws:ecs", "svc");
cluster(azB, region, "AZ-B");
node(b1, azB, "aws:ecs", "svc"); node(b2, azB, "aws:ecs", "svc"); node(b3, azB, "aws:ecs", "svc");
cluster(azC, region, "AZ-C");
node(c1, azC, "aws:ecs", "svc"); node(c2, azC, "aws:ecs", "svc"); node(c3, azC, "aws:ecs", "svc");
cluster(database, platform, "DATABASE CLUSTER");
node(primary, database, "aws:rds", "primary");
node(replica1, database, "aws:rds", "replica");
node(replica2, database, "aws:rds", "replica");
node(cache, database, "aws:elasticache", "cache");
cluster(observability, platform, "MONITORING");
node(metrics, observability, "aws:cloudwatch", "metrics");
node(logs, observability, "aws:cloudwatch", "logs");
node(alarms, observability, "aws:cloudwatch", "alarms");
connect(dnsToLb, dns, lb); // resolve, then hit the edge load balancer
connect(lbToGw, lb, gateway); // balanced across the API gateway
connect(toA, gateway, azA); // gateway fans out to each zone's services
connect(toB, gateway, azB);
connect(toC, gateway, azC);
connect(toWrite, a1, primary); // a representative service writes the primary
connect(toRead, a1, replica1); // reads served from a replica
connect(toCache, a1, cache);
connect(repl1, primary, replica1); // primary replicates to both replicas
connect(repl2, primary, replica2);
connect(toMetrics, b1, metrics); // services emit telemetry to monitoring
color(dnsToLb, cyan); color(dnsToLb.hot, cyan); // request path
color(lbToGw, cyan); color(lbToGw.hot, cyan);
color(toA, cyan); color(toA.hot, cyan);
color(toB, cyan); color(toB.hot, cyan);
color(toC, cyan); color(toC.hot, cyan);
color(toRead, cyan); color(toRead.hot, cyan);
color(toCache, cyan); color(toCache.hot, cyan);
color(toWrite, magenta); color(toWrite.hot, magenta); // write path
color(repl1, lime); color(repl1.hot, lime); // replication
color(repl2, lime); color(repl2.hot, lime);
color(toMetrics, gold); color(toMetrics.hot, gold); // telemetry
request(call, dns, "GET /order");
hidden(platform.nodes);
hidden(region.parts); hidden(azA.parts); hidden(azB.parts); hidden(azC.parts);
hidden(database.parts); hidden(observability.parts);
hidden(call.parts);
untraced(dnsToLb); untraced(lbToGw);
untraced(toA); untraced(toB); untraced(toC);
untraced(toWrite); untraced(toRead); untraced(toCache);
untraced(repl1); untraced(repl2); untraced(toMetrics);
step("edge") {
par {
stagger(0.08) { show(dns, 0.4); show(lb, 0.4); show(gateway, 0.4); }
say(caption, "Route 53 resolves the name, an edge load balancer spreads the traffic, the gateway takes it from there.", 0.4);
}
}
wait(0.2);
step("reveal-zones") {
par {
show(region.parts, 0.4);
show(azA.parts, 0.4); stagger(0.05) { show(a1,0.3); show(a2,0.3); show(a3,0.3); }
show(azB.parts, 0.4); stagger(0.05) { show(b1,0.3); show(b2,0.3); show(b3,0.3); }
show(azC.parts, 0.4); stagger(0.05) { show(c1,0.3); show(c2,0.3); show(c3,0.3); }
say(caption, "Three availability zones, each with its own services — nested clusters, auto-laid.", 0.4);
}
}
wait(0.2);
step("state-and-monitoring") {
par {
show(database.parts, 0.4); show(primary, 0.4); show(replica1, 0.4); show(replica2, 0.4); show(cache, 0.4);
show(observability.parts, 0.4); show(metrics, 0.4); show(logs, 0.4); show(alarms, 0.4);
say(caption, "A replicated database cluster and monitoring join in — the whole diagram re-fits.", 0.4);
}
}
wait(0.25);
step("topology") {
par {
stagger(0.06) {
draw(dnsToLb,0.4); draw(lbToGw,0.4);
draw(toA,0.4); draw(toB,0.4); draw(toC,0.4);
draw(toWrite,0.4); draw(toRead,0.4); draw(toCache,0.4);
draw(repl1,0.4); draw(repl2,0.4); draw(toMetrics,0.4);
}
say(caption, "Paths coloured by relationship: request cyan, writes magenta, replication lime, telemetry gold.", 0.4);
}
}
wait(0.3);
step("one-request") {
par {
seq {
show(call.parts, 0.2);
route(call, dnsToLb, 0.7, smooth);
route(call, lbToGw, 0.7, smooth);
route(call, toA, 0.7, smooth);
route(call, toWrite, 0.7, smooth);
}
say(caption, "One order: Route 53 → load balancer → gateway → an AZ-A service → the primary database.", 0.4);
}
}
wait(0.3);
step("takeaway") {
par {
pulse(region.parts, 0.7);
pulse(database.parts, 0.7);
say(caption, "Add a tier, a zone, a replica — you never touch a coordinate. That is manic.", 0.45);
}
}
wait(1.4);
Pass explicit (center, width, height) only when you deliberately want a
hand-placed diagram; it is never required to make one fit.
Flowcharts — a diagram that runs
An architecture lays out regions and clusters; a flowchart ranks its nodes by
the edges between them (like Mermaid’s graph TD/LR) and — this is manic’s edge
— it runs: a token walks the process and takes a branch. Just declare
flowchart(id): like an architecture it auto-fits and needs no coordinates. It
lays the flow top-down and, when the flow is long, wraps it into side-by-side
columns — the count chosen to fill the frame — so every node stays full-size and
readable: you read down one column, then across to the top of the next, like a
multi-column diagram on paper. Forward edges are clean elbows; a long feedback loop
routes neatly around the bottom-left perimeter instead of cutting across the middle.
Pass flowchart(id, LR) to force a single left-to-right row instead.
A flowchart is only useful if you can read its nodes, so there is a readability
limit — 26 nodes top-down, 14 left-right — and past it (more than even column
wrapping keeps legible) the editor warns you to split the process into linked
sub-flows: end one chart with a connector node that hands off to the next.
Raise the limit with a third argument, flowchart(id, dir, N), when you
deliberately want a denser chart.
Every node is the same node(id, parent, "kind", "label") you already know — only
now the kind is a shape: terminator (a start/end pill), process (a step
rectangle), decision (a diamond), io (a parallelogram), subprocess, or
connector (a small circle). The shape is the node’s body, with the label
centred inside — nothing new to learn beyond the shape names.
Connect the nodes; inside a flowchart the edges default to clean orthogonal elbow
connectors along the rank direction. Give a decision’s branches captions with
annotate(edge, "yes"), colour them by meaning, and route a token from the
start terminator to watch the algorithm execute. A loop is just an edge back to an
earlier node (give it explicit ports so it routes up the side):
title("A Flowchart That Runs — the Factorial Loop");
canvas("16:9");
template("paper");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic");
text(kicker, (cx, h*0.06), "DIAGRAMS · FLOWCHART · AUTO-LAID");
text(headline, (cx, h*0.12), "A flowchart that runs");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 26*u); bold(headline); wrap(headline, w*0.86);
// `flowchart(fc)` ranks nodes by their connections, auto-fits, and auto-orients:
// a shallow flow like this stays top-down on its own. No coordinates, no direction
// to choose. Node shapes are string kinds.
flowchart(fc);
node(start, fc, "terminator", "start");
node(rd, fc, "io", "read n");
node(init, fc, "process", "f=1 i=1");
node(dec, fc, "decision", "i <= n?");
node(body, fc, "process", "f=f*i i=i+1");
node(out, fc, "io", "print f");
node(fin, fc, "terminator", "end");
connect(e1, start, rd);
connect(e2, rd, init);
connect(e3, init, dec);
connect(e4, dec, body); // keep looping
connect(e5, dec, out); // exit
connect(e6, body, dec, orthogonal, right, right); // loop back up the side
connect(e7, out, fin);
color(e4, cyan); color(e4.hot, cyan);
color(e5, lime); color(e5.hot, lime);
color(e6, gold); color(e6.hot, gold);
annotate(e4, "yes"); annotate(e5, "no"); annotate(e6, "loop");
// A colour key stands in for narration — the diagram explains itself.
text(k1, (w*0.34, h*0.93), "— continue"); size(k1, 14*u); color(k1, cyan); bold(k1);
text(k2, (w*0.50, h*0.93), "— exit"); size(k2, 14*u); color(k2, lime); bold(k2);
text(k3, (w*0.66, h*0.93), "— loop back"); size(k3, 14*u); color(k3, gold); bold(k3);
request(tok, start, "n = 3");
hidden(fc.nodes);
hidden(tok.parts);
step("shape") {
stagger(0.07) {
show(start, 0.3); show(rd, 0.3); show(init, 0.3); show(dec, 0.3);
show(body, 0.3); show(out, 0.3); show(fin, 0.3);
}
}
wait(0.2);
step("run") {
seq {
show(tok.parts, 0.2);
route(tok, e1, 0.4, smooth); route(tok, e2, 0.4, smooth); route(tok, e3, 0.4, smooth);
route(tok, e4, 0.4, smooth); route(tok, e6, 0.5, smooth);
route(tok, e4, 0.4, smooth); route(tok, e6, 0.5, smooth);
route(tok, e4, 0.4, smooth); route(tok, e6, 0.5, smooth);
route(tok, e5, 0.4, smooth); route(tok, e7, 0.4, smooth);
}
}
wait(0.3);
step("takeaway") {
seq { pulse(fin, 0.7); pulse(fin, 0.7); }
}
wait(1.0);
Start without a cloud provider
The foundation includes native visual archetypes for client, service,
gateway, database, cache, queue, storage, and external. These names
select a compact visual only; they still imply no runtime behavior:
architecture(platform, (cx,cy), w*0.88, h*0.58);
node(user, platform, "client", "User");
node(edge, platform, "gateway", "Gateway");
node(api, platform, "service", "API Service");
node(db, platform, "database", "Database");
Use provider names only when recognizable artwork helps the lesson. A generic diagram remains fully portable and needs no image assets.
The complete foundation story sends one request forward and returns the same identity over separately directed paths:
title("Systems Foundation — One Request, One Return");
canvas("16:9");
template("mono");
watermark(mark, (160, 54), "Made With Manic");
text(kicker, (cx, 54), "PROVIDER-NEUTRAL SYSTEMS");
text(headline, (cx, 104), "Structure is not behaviour");
text(caption, (cx, h-58), "Cold paths show what is possible. One persistent dot shows what actually happened.");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
size(caption, 20); color(caption, dim); wrap(caption, w*0.86);
architecture(platform, (cx, cy+15), w*0.88, h*0.58);
node(user, platform, "client", "User");
node(edge, platform, "gateway", "Gateway");
node(api, platform, "service", "API Service");
node(cache, platform, "cache", "Cache");
node(db, platform, "database", "Database");
connect(toEdge, user, edge);
connect(toApi, edge, api);
connect(toCache, api, cache);
// One semantic connection: Manic chooses node-boundary ports and keeps the
// packet continuous across every internal orthogonal segment.
connect(toDb, cache, db, orthogonal, right, top);
connect(dbReturn, db, api, 95);
connect(apiReturn, api, edge, 75);
connect(edgeReturn, edge, user, 55);
message(packet, user, "GET");
hidden(kicker); hidden(headline); hidden(caption);
untraced(platform.connections);
hidden(packet);
step("structure") {
par {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.45);
draw(platform.connections, 1.20, smooth);
}
}
wait(0.45);
step("continuous-round-trip") {
par {
show(packet, 0.20);
say(caption, "One dot moves continuously end to end. Every route hand-off preserves its identity.", 0.35);
seq {
route(packet, toEdge, 0.70, linear);
route(packet, toApi, 0.70, linear);
route(packet, toCache, 0.70, linear);
route(packet, toDb, 0.70, linear);
par {
recolor(packet, magenta, 0.30);
route(packet, dbReturn, 0.80, linear);
}
route(packet, apiReturn, 0.75, linear);
route(packet, edgeReturn, 0.75, linear);
}
}
}
wait(0.45);
step("proof") {
par {
pulse(packet, 0.70);
say(caption, "The same dot is back at the User: one identity, seven connected journeys, zero animation gaps.", 0.40);
}
}
wait(1.10);
Declare the parent first
A node’s second argument is its parent architecture or cluster. Clusters may themselves belong to an earlier cluster, giving hierarchy without member lists or special block syntax:
architecture(events, (cx,cy), w*0.9, h*0.7);
node(source, events, "aws:eks", "K8s Source");
cluster(flows, events, "EVENT FLOWS");
cluster(workers, flows, "EVENT WORKERS");
node(worker1, workers, "aws:ecs", "Worker 1");
node(worker2, workers, "aws:ecs", "Worker 2");
node(worker3, workers, "aws:ecs", "Worker 3");
node(queue, flows, "aws:sqs", "Event Queue");
cluster(processing, flows, "PROCESSING");
node(proc1, processing, "aws:lambda", "Processor 1");
node(proc2, processing, "aws:lambda", "Processor 2");
node(proc3, processing, "aws:lambda", "Processor 3");
node(store, events, "aws:s3", "Events Store");
node(analytics, events, "aws:redshift", "Analytics");
Landscape uses a horizontal system flow and stacks replicated members inside their clusters. Portrait rotates the main flow and places replicated members across the available width. Cluster frames resize from their descendants.
Cold topology, then hot behaviour
connect(toWorkers, source, workers);
connect(toQueue, workers, queue);
connect(toProcessors, queue, processing);
connect(toStore, processing, store);
message(event, source, "EVENT");
step("process") {
seq {
route(event, toWorkers, 0.8, smooth);
route(event, toQueue, 0.8, smooth);
route(event, toProcessors, 0.8, smooth);
}
}
Connecting a node to a cluster creates the possible lane to every member. Connections are dashed by default: they explain a relationship without falsely claiming that data is flowing. The hot overlay stays solid when activity begins.
The declared connection name addresses the complete fan-out or fan-in group, so
draw(toWorkers) and flow(toWorkers, ...) affect every physical lane. route
still selects only the lane beginning at the message’s current node.
route is the explicit form. It chooses the physical lane that begins at the
message’s current node, illuminates that lane, moves the same message identity,
and records its semantic destination. A later route that does not start there
fails during manic check instead of teleporting.
Use draw(toWorkers) to reveal every possible lane. Use route for one real
message. Use flow(toWorkers, ...) only when the idea is aggregate activity
across the worker pool.
Route around a visual obstacle
Connections remain direct unless the creator supplies a signed bend in canvas units:
connect(toDatabase, services, database);
connect(toCache, services, cache, 145*u);
The optional fourth argument changes only the drawn curve and the route that travels over it. Positive and negative values curve on opposite sides. It does not infer obstacles, architecture semantics, or provider behavior; use the smallest bend that keeps the diagram readable on every target canvas.
Use one orthogonal connector
For architecture diagrams that need right-angle routing, keep the connection as one semantic identity:
connect(toDatabase, cache, database, orthogonal);
connect(returnPath, database, api, orthogonal, bottom, right);
message(packet, cache, "GET");
route(packet, toDatabase, 0.9, linear);
orthogonal uses automatic node-boundary ports by default. Add explicit source
and destination ports only when the composition needs them: left, right,
top, or bottom. The engine builds the internal elbows, but draw, flow,
route, and hotpath still address the declared connection id. One message
moves across every segment without replacement objects or animation gaps.
This is deterministic Manhattan geometry, not obstacle avoidance. Manic does not inspect service kinds or guess what the connection means.
The connection & arrow grammar
systems-arrow-patterns.manic is the visual reference for every way one
connection can be drawn. All six share one rule: the connection describes a
possible relationship; direction and meaning come from the moving object
(route/flow) and from creator-chosen colour, never from the arrowhead
alone.
| # | Pattern | How to author it |
|---|---|---|
| 1 | One-way — one source, one destination | connect(a2b, a, b) |
| 2 | Round-trip — request and response as separate, honest lanes | connect(fwd, a, b) + connect(ret, b, a) (style/colour them differently) |
| 3 | Orthogonal — right-angle Manhattan routing, one identity | connect(a2b, a, b, orthogonal) |
| 4 | Vertical ports — the lane enters/leaves a chosen face | connect(a2b, a, b, orthogonal, top, bottom) |
| 5 | Fan-out — N explicitly authored deliveries (not inferred broadcast) | connect(toW1, q, w1) … connect(toW3, q, w3), or one connect(toWorkers, q, workers) to a cluster |
| 6 | Diagonal duplex — two directions with distinct styling | two connects + per-lane color/hue/dashed |
Colour the lanes by relationship so a dense diagram reads at a glance — each
connection exposes the cold line id and the hot overlay id.hot:
color(fwd, cyan); color(fwd.hot, cyan); // request
hue(ret, 328); hue(ret.hot, 328); // response (pink)
Nothing here implies balancing, buffering, or broadcast. A fan-out is possible lanes; the message that moves — and the colour you give each lane — is what tells the runtime story.
Translate a Mingrammer diagram
The structural mapping is deliberately small:
| Mingrammer | Manic |
|---|---|
Diagram(...) | architecture(...) |
Cluster(...) | cluster(...) |
ECS(...), RDS(...), and other provider objects | node(..., "aws:kind", ...) |
a >> b | connect(path, a, b) |
a - b | link(path, a.card, b.card) |
| runtime behavior | explicit message, route, travel, or flow |
This keeps the imported design faithful without turning labels such as “lb,”
“queue,” or “topic” into hidden behavior. The creator decides which object
moves, which path it takes, and whether several motions use seq or par.
The clustered web-services example recreates Route 53, ELB, three ECS services, an RDS primary/read-only pair, and ElastiCache. It then distinguishes all cold relationships from one database round trip, one cache round trip, and a separately authored database-link pulse. Return journeys use separately directed connections and recolour the same persistent identity rather than playing the incoming path backward. The example gives responses a second visual grammar—curved pink dashed arrows, a heavier glow, and a matching response identity—while requests remain clean solid strokes:
title("Clustered Web Services on AWS");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.025), "Made With Manic");
text(kicker, (cx, h*0.055), "AWS ARCHITECTURE · CLUSTERED WEB SERVICES");
text(headline, (cx, h*0.115), "Requests in. Responses back.");
text(caption, (cx, h*0.935), "Dashed paths describe what is connected—not what is moving now.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 29*u); bold(headline); wrap(headline, w*0.88);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86);
if h > 1.25*w {
architecture(clusteredWeb, (cx, h*0.51), w*0.84, h*0.70);
text(requestKey, (w*0.36, h*0.225), "REQUEST");
arrow(requestSample, (w*0.48, h*0.225), (w*0.64, h*0.225));
text(responseKey, (w*0.36, h*0.265), "RESPONSE");
arrow(responseSample, (w*0.48, h*0.265), (w*0.64, h*0.265));
}
else {
architecture(clusteredWeb, (cx, h*0.51), w*0.94, h*0.69);
text(requestKey, (w*0.105, h*0.205), "REQUEST");
arrow(requestSample, (w*0.15, h*0.205), (w*0.205, h*0.205));
text(responseKey, (w*0.105, h*0.245), "RESPONSE");
arrow(responseSample, (w*0.15, h*0.245), (w*0.205, h*0.245));
}
size(requestKey, 15*u); color(requestKey, fg); bold(requestKey);
size(responseKey, 15*u); hue(responseKey, 328, 0.90, 0.62); bold(responseKey);
color(requestSample, fg); stroke(requestSample, 3*u);
hue(responseSample, 328, 0.90, 0.62); dashed(responseSample, 10*u, 7*u);
stroke(responseSample, 4*u); glow(responseSample, 0.85);
hidden(requestKey); hidden(requestSample);
hidden(responseKey); hidden(responseSample);
node(dns, clusteredWeb, "aws:route53", "dns");
node(lb, clusteredWeb, "aws:elb", "lb");
cluster(services, clusteredWeb, "SERVICES");
node(web1, services, "aws:ecs", "web1");
node(web2, services, "aws:ecs", "web2");
node(web3, services, "aws:ecs", "web3");
cluster(database, clusteredWeb, "DB CLUSTER");
node(dbPrimary, database, "aws:rds", "userdb");
node(dbReplica, database, "aws:rds", "userdb ro");
node(memcached, clusteredWeb, "aws:elasticache", "memcached");
connect(dnsToLb, dns, lb);
connect(lbToServices, lb, services);
connect(servicesToDb, services, dbPrimary);
connect(servicesToCache, services, memcached, 145*u);
connect(dbToServices, dbPrimary, services, 42*u);
connect(servicesToLb, services, lb, 42*u);
connect(cacheToWeb2, memcached, web2, 145*u);
hue(dbToServices.hot, 328, 0.90, 0.62);
hue(servicesToLb.hot, 328, 0.90, 0.62);
hue(cacheToWeb2.hot, 328, 0.90, 0.62);
dashed(dbToServices.hot, 10*u, 7*u);
dashed(servicesToLb.hot, 10*u, 7*u);
dashed(cacheToWeb2.hot, 10*u, 7*u);
stroke(dbToServices.hot, 4.5*u); glow(dbToServices.hot, 0.95);
stroke(servicesToLb.hot, 4.5*u); glow(servicesToLb.hot, 0.95);
stroke(cacheToWeb2.hot, 4.5*u); glow(cacheToWeb2.hot, 0.95);
link(replication, dbPrimary.card, dbReplica.card);
dashed(replication); color(replication, dim); stroke(replication, 2.5*u);
message(pageRequest, dns, "GET");
message(cacheRead, web2, "READ");
hidden(clusteredWeb.nodes);
hidden(services.parts); hidden(database.parts);
hidden(pageRequest.parts); hidden(cacheRead.parts);
untraced(dnsToLb); untraced(lbToServices);
untraced(servicesToDb); untraced(servicesToCache);
untraced(dbToServices); untraced(servicesToLb); untraced(cacheToWeb2);
untraced(replication);
step("entry") {
par {
show(dns, 0.40);
show(lb, 0.40);
say(caption, "Route 53 and the load balancer form the public entry into the design.", 0.40);
}
}
wait(0.20);
step("service-pool") {
par {
show(services.parts, 0.45);
stagger(0.10) {
show(web1, 0.35);
show(web2, 0.35);
show(web3, 0.35);
}
say(caption, "Three ECS services share one ownership boundary; no runtime choice is implied yet.", 0.40);
}
}
wait(0.20);
step("state") {
par {
show(database.parts, 0.45);
show(dbPrimary, 0.40);
show(dbReplica, 0.40);
show(memcached, 0.40);
say(caption, "The database cluster and cache complete the static architecture.", 0.40);
}
}
wait(0.20);
step("possible-topology") {
par {
draw(dnsToLb, 0.40);
draw(lbToServices, 0.60);
draw(servicesToDb, 0.60);
draw(servicesToCache, 0.60);
draw(replication, 0.45);
show(requestKey, 0.35); show(requestSample, 0.35);
show(responseKey, 0.35); show(responseSample, 0.35);
say(caption, "Every dashed lane is possible. The next moving object will identify the path actually used.", 0.40);
}
}
wait(0.30);
step("request-to-database") {
par {
seq {
show(pageRequest.parts, 0.20);
route(pageRequest, dnsToLb, 0.65, smooth);
route(pageRequest, lbToServices, 0.90, smooth);
route(pageRequest, servicesToDb, 0.90, smooth);
}
say(caption, "This request follows one authored lane: dns → lb → web1 → userdb.", 0.40);
}
}
wait(0.35);
step("database-response") {
par {
seq {
par {
say(pageRequest.label, "200 OK", 0.25);
to(pageRequest.parts, hue, 328, 0.25, smooth);
}
route(pageRequest, dbToServices, 0.90, smooth);
route(pageRequest, servicesToLb, 0.90, smooth);
}
say(caption, "The same identity returns userdb → web1 → lb. DNS resolved the name; it is not the HTTP response path.", 0.40);
}
}
wait(0.35);
step("cache-round-trip") {
par {
seq {
show(cacheRead.parts, 0.20);
route(cacheRead, servicesToCache, 1.05, smooth);
par {
say(cacheRead.label, "HIT", 0.25);
to(cacheRead.parts, hue, 328, 0.25, smooth);
}
route(cacheRead, cacheToWeb2, 1.05, smooth);
}
say(caption, "A separate cache read travels web2 → memcached, becomes HIT, and returns to that same service.", 0.40);
}
}
wait(0.35);
step("replica-relationship") {
par {
flow(replication, 3.20, both, continuous);
say(caption, "The neutral database link can carry a two-way visual pulse only because the creator asked for it.", 0.40);
}
}
wait(0.35);
step("takeaway") {
par {
pulse(services.parts, 0.70);
pulse(database.parts, 0.70);
say(caption, "Architecture stays readable; objects, paths, timing, and composition tell the runtime story.", 0.45);
}
}
wait(1.40);
Infer one complete hot path
For the common architecture-explainer shot, Manic can follow the graph itself:
message(event, source, "EVENT");
step("runtime") {
hotpath(event, 6.0, 27);
}
hotpath(message, [duration], [seed]) begins at the message’s current node,
finds valid outgoing physical lanes, chooses one at every fan-out, and keeps the
same dot moving until it reaches a sink. Only the selected lanes illuminate;
all other relationships stay dashed and quiet.
The optional seed controls the branch choices. The result feels random to a
viewer but is deterministic across previews, backend renders, and WASM. Change
the seed to show another valid execution without rewriting the story. Use
route when the exact service sequence is part of the lesson; use hotpath
when the lesson is “one possible runtime through this topology.”
Directional and continuous flow
flow(path, 0.8); // forward, one clean pulse
flow(path, 4.0, forward, continuous); // finite repeating stream
flow(returnPath, 1.0, reverse, once); // reverse pulse on generic geometry
flow(syncPath, 4.0, both, continuous); // independent duplex streams
The full signature is:
flow(path, [duration], [forward|reverse|both], [once|continuous])
Continuous flow chooses an integer number of length-aware cycles. It begins empty and ends on a cycle boundary, so seeking remains deterministic and the stream drains instead of freezing halfway along the connection. In a Systems story, prefer a separately directed return connection when the topology truly contains a response path.
Complete event-processing story
title("Event Processing on AWS — From Topology to Hot Path");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.055), "Made With Manic");
text(kicker, (cx, h*0.055), "AWS ARCHITECTURE · EVENT PROCESSING");
text(headline, (cx, h*0.115), "One topology. One event. One visible hot path.");
text(caption, (cx, h*0.935), "Dashed relationships show every route the architecture allows.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 29*u); bold(headline); wrap(headline, w*0.88);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86);
if h > 1.25*w {
architecture(eventProcessing, (cx, h*0.51), w*0.82, h*0.70);
}
else {
architecture(eventProcessing, (cx, h*0.51), w*0.92, h*0.69);
}
node(source, eventProcessing, "aws:eks", "K8s Source");
cluster(flows, eventProcessing, "EVENT FLOWS");
cluster(workers, flows, "EVENT WORKERS");
node(worker1, workers, "aws:ecs", "Worker 1");
node(worker2, workers, "aws:ecs", "Worker 2");
node(worker3, workers, "aws:ecs", "Worker 3");
node(queue, flows, "aws:sqs", "Event Queue");
cluster(processing, flows, "PROCESSING");
node(proc1, processing, "aws:lambda", "Processor 1");
node(proc2, processing, "aws:lambda", "Processor 2");
node(proc3, processing, "aws:lambda", "Processor 3");
node(store, eventProcessing, "aws:s3", "Events Store");
node(warehouse, eventProcessing, "aws:redshift", "Analytics");
connect(toWorkers, source, workers);
connect(toQueue, workers, queue);
connect(toHandlers, queue, processing);
connect(toStore, processing, store);
connect(toAnalytics, processing, warehouse);
message(event, source, "EVENT");
hidden(eventProcessing.nodes);
hidden(flows.parts); hidden(workers.parts); hidden(processing.parts);
hidden(event.parts);
untraced(toWorkers); untraced(toQueue); untraced(toHandlers);
untraced(toStore); untraced(toAnalytics);
step("source") {
par {
show(source, 0.45);
say(caption, "EKS is the source; the rest of the system is still only structure.", 0.40);
}
}
wait(0.25);
step("ownership") {
par {
show(flows.parts, 0.50);
show(workers.parts, 0.45);
stagger(0.10) { show(worker1, 0.35); show(worker2, 0.35); show(worker3, 0.35); }
show(queue, 0.40);
show(processing.parts, 0.45);
stagger(0.10) { show(proc1, 0.35); show(proc2, 0.35); show(proc3, 0.35); }
show(store, 0.40); show(warehouse, 0.40);
say(caption, "Nested boundaries reveal ownership without hiding the individual services.", 0.40);
}
}
wait(0.20);
step("possible-topology") {
par {
draw(toWorkers, 0.55);
draw(toQueue, 0.55);
draw(toHandlers, 0.55);
draw(toStore, 0.55); draw(toAnalytics, 0.55);
say(caption, "Every dashed edge is a possible relationship—not a claim that data is flowing.", 0.40);
}
}
wait(0.35);
step("runtime-hot-path") {
par {
show(event.parts, 0.25);
hotpath(event, 6.20, 27);
say(caption, "The event now chooses valid branches and travels end-to-end as one continuous hot path.", 0.40);
}
}
wait(0.35);
step("takeaway") {
par {
pulse(event.parts, 0.70);
say(caption, "Cold topology explains relationships. The moving dot proves what happened now.", 0.45);
}
}
wait(1.40);
Run and audit it directly:
manic examples/aws-event-processing-clusters-poc.manic
manic check examples/aws-event-processing-clusters-poc.manic --canvas all
What the kit will not do for you
By design, the kit never infers behaviour from an icon or a label. It will not
decide that a node is a load balancer, a queue, or a database, and it never
simulates balancing, buffering, broadcast, retries, or failure. You author what
happens with explicit route, flow, seq, par, colour, and ordinary verbs
— that is what keeps the vocabulary small and the diagram honest.
Two practical tips:
- Draw the return path, don’t reverse the arrow. A response is its own connection with its own colour, not the request lane played backward.
- For very dense diagrams, shape the layout yourself. Group nodes into clusters and split wide rows into columns so nothing overflows the frame.
Diagrams — icon reference & aliases
Every diagram node takes a provider:name string that selects artwork only — it never implies routing, balancing, queueing, or persistence. The catalogue covers 17 providers / 2461 components, generated from assets/diagrams/ by scripts/gen-diagram-manifest.py (Mingrammer/Diagrams icon set, disk-loaded at render — no binary bloat).
Reference notation
provider:name— e.g.node(x, arch, "aws:lambda", "fn"),"gcp:bigquery","onprem:redis","k8s:pod". First matching category wins for the bare name.provider:category/name— disambiguates when the same name lives in two categories of one provider, e.g."aws:network/internet-gateway"vs"aws:general/internet-gateway".- Native archetypes need no provider:
client,service,gateway,database,cache,queue,storage,external(no cloud assets).
Friendly aliases
Short, human names resolve to the exact catalogue key — same artwork either way:
| Alias | Resolves to |
|---|---|
aws:apigateway | aws:api-gateway |
aws:route53 | aws:route-53 |
aws:elb | aws:elastic-load-balancing |
aws:load-balancer | aws:elastic-load-balancing |
aws:sqs | aws:simple-queue-service-sqs |
aws:s3 | aws:simple-storage-service-s3-bucket |
aws:ecs | aws:elastic-container-service |
aws:eks | aws:elastic-kubernetes-service |
Everything else passes through unchanged: aws:lambda, gcp:*, azure:*, onprem:*, k8s:*, ibm:*, oci:*, … all resolve directly.
Colouring paths by relationship
A connect(id, …) exposes two colourable parts — the cold dashed line id and the hot overlay id.hot. Group connections by relationship and colour each group (color/hue/glow/dashed/stroke) so a dense diagram reads at a glance:
color(reqPath, cyan); color(reqPath.hot, cyan); // REQUEST
color(logPath, gold); color(logPath.hot, gold); // ANALYTICS
hue(scrape, 328); hue(scrape.hot, 328); // TELEMETRY (pink)
Providers & components
Any listed name works as provider:name. Full machine-readable catalogue: assets/diagrams-manifest.json.
alibabacloud — 92 components
alibabacloud:alibabacloud, alibabacloud:analytic-db, alibabacloud:anti-bot-service, alibabacloud:anti-ddos-basic, alibabacloud:anti-ddos-pro, alibabacloud:antifraud-service, alibabacloud:api-gateway, alibabacloud:apsaradb-cassandra, alibabacloud:apsaradb-hbase, alibabacloud:apsaradb-memcache, alibabacloud:apsaradb-mongodb, alibabacloud:apsaradb-oceanbase, alibabacloud:apsaradb-polardb, alibabacloud:apsaradb-postgresql … (+78 more)
aws — 513 components
aws:ad-connector, aws:alexa-for-business, aws:amazon-devops-guru, aws:amazon-managed-grafana, aws:amazon-managed-prometheus, aws:amazon-managed-workflows-apache-airflow, aws:amazon-opensearch-service, aws:amplify, aws:analytics, aws:apache-mxnet-on-aws, aws:api-gateway, aws:api-gateway-endpoint, aws:app-mesh, aws:app-runner … (+499 more)
azure — 690 components
azure:aad-licenses, azure:abs-member, azure:access-review, azure:active-directory, azure:active-directory-connect-health, azure:activity-log, azure:ad-b2c, azure:ad-domain-services, azure:ad-identity-protection, azure:ad-privileged-identity-management, azure:administrative-units, azure:advisor, azure:ai-studio, azure:aks-istio … (+676 more)
digitalocean — 26 components
digitalocean:certificate, digitalocean:containers, digitalocean:dbaas-primary, digitalocean:dbaas-primary-standby-more, digitalocean:dbaas-read-only, digitalocean:dbaas-standby, digitalocean:digitalocean, digitalocean:docker, digitalocean:domain, digitalocean:domain-registration, digitalocean:droplet, digitalocean:droplet-connect, digitalocean:droplet-snapshot, digitalocean:firewall … (+12 more)
elastic — 42 components
elastic:agent, elastic:alerting, elastic:apm, elastic:app-search, elastic:auditbeat, elastic:beats, elastic:cloud, elastic:crawler, elastic:ece, elastic:eck, elastic:elastic, elastic:elasticsearch, elastic:endpoint, elastic:enterprise-search … (+28 more)
firebase — 22 components
firebase:ab-testing, firebase:app-distribution, firebase:app-indexing, firebase:authentication, firebase:crash-reporting, firebase:crashlytics, firebase:dynamic-links, firebase:extensions, firebase:firebase, firebase:firestore, firebase:functions, firebase:hosting, firebase:in-app-messaging, firebase:invites … (+8 more)
gcp — 123 components
gcp:access-context-manager, gcp:advanced-solutions-lab, gcp:ai-hub, gcp:ai-platform, gcp:ai-platform-data-labeling-service, gcp:api-gateway, gcp:apigee, gcp:app-engine, gcp:armor, gcp:assured-workloads, gcp:automl, gcp:automl-natural-language, gcp:automl-tables, gcp:automl-translation … (+109 more)
generic — 27 components
generic:android, generic:blank, generic:centos, generic:datacenter, generic:debian, generic:firewall, generic:generic, generic:ios, generic:linux-general, generic:mobile, generic:qemu, generic:rack, generic:raspbian, generic:red-hat … (+13 more)
gis — 65 components
gis:actinia, gis:addok, gis:ban, gis:baremaps, gis:cesium, gis:deegree, gis:g3w-suite, gis:gdal, gis:geohealthcheck, gis:geomapfish, gis:geomesa, gis:geonetwork, gis:geonode, gis:geopackage … (+51 more)
ibm — 164 components
ibm:actionable-insight, ibm:alert-notification, ibm:analytics, ibm:annotate, ibm:api-developer-portal, ibm:api-management, ibm:api-polyglot-runtimes, ibm:api-security, ibm:app-server, ibm:application-logic, ibm:artifact-management, ibm:bare-metal-server, ibm:block-storage, ibm:blockchain … (+150 more)
k8s — 46 components
k8s:api, k8s:c-c-m, k8s:c-m, k8s:c-role, k8s:chaos-mesh, k8s:cm, k8s:crb, k8s:crd, k8s:cronjob, k8s:deploy, k8s:ds, k8s:ep, k8s:etcd, k8s:external-dns … (+32 more)
oci — 141 components
oci:alarm, oci:alarm-white, oci:api-gateway, oci:api-gateway-white, oci:api-service, oci:api-service-white, oci:audit, oci:audit-white, oci:autonomous, oci:autonomous-white, oci:autoscale, oci:autoscale-white, oci:backbone, oci:backbone-white … (+127 more)
onprem — 172 components
onprem:activemq, onprem:aerospike, onprem:airflow, onprem:ambassador, onprem:ansible, onprem:apache, onprem:argocd, onprem:atlantis, onprem:awx, onprem:beam, onprem:bind-9, onprem:bitwarden, onprem:boundary, onprem:buzzfeed-sso … (+158 more)
openstack — 51 components
openstack:ansible, openstack:barbican, openstack:blazar, openstack:charms, openstack:chef, openstack:cinder, openstack:cloudkitty, openstack:congress, openstack:cyborg, openstack:designate, openstack:ec2api, openstack:freezer, openstack:glance, openstack:heat … (+37 more)
outscale — 13 components
outscale:client-vpn, outscale:compute, outscale:direct-connect, outscale:firewall, outscale:identity-and-access-management, outscale:internet-service, outscale:load-balancer, outscale:nat-service, outscale:net, outscale:outscale, outscale:simple-storage-service, outscale:site-to-site-vpng, outscale:storage
programming — 74 components
programming:action, programming:angular, programming:backbone, programming:bash, programming:c, programming:camel, programming:collate, programming:cpp, programming:csharp, programming:dapr, programming:dart, programming:database, programming:decision, programming:delay … (+60 more)
saas — 40 components
saas:adyen, saas:akamai, saas:amazon-pay, saas:auth0, saas:cloudflare, saas:cloudinary, saas:crowdstrike, saas:datadog, saas:dataform, saas:discord, saas:facebook, saas:fastly, saas:imperva, saas:intercom … (+26 more)
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.
Generated and derived 3D geometry
These constructors cover large scientific structures without expanding the scene into thousands of named objects:
implicit3(shell,"x*x+y*y-z*z",(-2,2),(-2,2),(-2,2),1,24);
collection3data(nodes,(0,0,0),"0 0 0; 1 0 0; 1 1 1; 0 1 0");
links3data(edges,nodes,"0 1; 1 2; 2 3; 3 0");
lsystem3(tree,(0,0,0),0.18,25,5,"F","F=F[+F][-F][^F]");
implicit3samples a bounded scalar field and extracts one isosurface.collection3dataandlinks3dataretain explicit coordinates and arbitrary edges as two batches—useful for Cayley diagrams, dependency graphs, and imported scientific datasets.lsystem3is a 3D turtle grammar:Fdraws,fmoves,+/-yaw,^/&pitch,</>roll, and brackets branch.
To show the three shadows of a moving curve, derive them from the source:
curve3(path,"2*cos(t)","2*sin(t)","t/3",(0,12.56));
projectpath3(xyShadow,path,xy);
projectpath3(xzShadow,path,xz);
projectpath3(yzShadow,path,yz);
par {
rotate3(path,(20,50,10),2,smooth);
orbit3(55,20,18,2,smooth);
}
The projected paths resample the source after its current transform, so the shadows cannot drift away from the geometry they explain.
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.
The creator-first five
For most explainers, start with the relationship you want rather than camera coordinates or intermediate positions. 3D V2 adds five composition words:
| word | creator intent |
|---|---|
view3(subject, "isometric", 1, smooth, 1.3) | frame one object or tagged group, with margin |
travel3(subject, route, 2, smooth) | move along a line3, arrow3, or curve3 |
attach3(label, subject, (0,0,1)) | position-only follow (compatible default); add rigid to inherit orientation; use none to release |
become3(subject, blueprint, 1, smooth) | keep the subject id while it adopts another shape and style |
turn3(group, pivot, z, 90, 1, smooth) | rigidly turn one object or tagged group around a pivot and axis |
Here is the core pattern. The sensor and wings travel with the ship, release without snapping, then the wings deploy and the ship becomes its final design:
attach3(sensor, ship, (0,0,0.8));
attach3(leftWing, ship, (0,-1.2,0));
attach3(rightWing, ship, (0,1.2,0));
par { draw(route, 2.8, smooth); travel3(ship, route, 2.8, smooth); }
attach3(sensor, none);
attach3(leftWing, none); attach3(rightWing, none);
par {
turn3(wings, ship, z, 90, 1.1, smooth);
become3(ship, finalBlueprint, 1.1, smooth);
}
view3(spacecraft, "isometric", 1.2, smooth, 1.4);
▶ Compact five-word reference:
▶ Vertical creator story:
▶ Production lab (relationships, contours, finishes, OBJ, and variable tube):
Design details that make 3D feel professional
- Give the main subject one persistent id. Declare alternate forms as hidden
blueprints and use
become3; do not swap the whole scene. - Tag parts that should turn or frame together.
view3andturn3both accept a tag, so one spatial rig remains one authoring concept. - Attach before the shared move; release immediately before a part gets its own motion. Release preserves the resolved world position.
- Use
view3(...,"fit")to preserve the current viewing direction, or choosefront,side,top, orisometric. A margin around1.15–1.35is useful for landscape; start around1.45–1.7for vertical video with text above and a social footer below. - Put shot changes at idea boundaries, then let object motion carry the middle of the explanation. Constant camera motion makes depth harder to read.
view3uses transformed group bounds and the active canvas aspect ratio, so the same intent works in landscape and portrait without a second 3D mode. In a creator/quiz scene it also fits the actual media rectangle, not the full canvas behind the heading and social footer.travel3reads the route’s current transform every frame. A route may rotate or move in the sameparblock without pre-baking dozens of coordinates.- Use
attach3(part, body, offset, rigid)for a mechanical assembly. Its offset lives in the body’s local frame, it turns with the body, and release freezes both position and orientation exactly.
▶ See it play:
Scientific frames: 2D, textbook 3D, and spatial 3D
Use the smallest truthful dimension. A genuinely planar geometry problem should
stay in Manic’s native 2D shapes. When the relationship is spatial but the
result belongs on a calm textbook page, use a real 3D frame3 with an
orthographic camera and mode=textbook. When depth becomes part of the lesson,
restyle that same frame and orbit it:
camera3((9,-11,8), (0,0,0), 12, orthographic);
frame3(world, (0,0,0), (8,8,6),
"x=-4..4 y=-4..4 z=-3..3 planes=xy:min,xz:min,yz:min major=2 minor=1 mode=textbook");
// Later: same coordinates, geometry, labels, and identity.
par {
present3(world, spatial, 0.7, smooth);
orbit3(52, 28, 12, 2.4, smooth);
}
The options are compact and composable:
| need | option |
|---|---|
| data bounds | x=-2..4 y=0..8 z=0.1..100 |
| one or more walls | planes=xy:min,xz:min,yz:min |
| a top or central plane | planes=xy:max,xz:origin |
| an exact section | planes=xy:-0.5 |
| two coloured parallel sections | planes=xy:-0.5@cyan,xy:1.5@magenta |
| shared intervals | major=1 minor=0.5 |
| independent intervals | xmajor=0.5 ymajor=1 zmajor=2 |
| logarithmic data | zscale=log (the corresponding range must be positive) |
| page or spatial styling | mode=textbook or mode=spatial |
Every generated role is addressable through tags: world.axes, world.axis.x,
world.grids, world.grid.xy, world.grid.major, world.grid.minor,
world.ticks, and world.labels. This makes ordinary show, fade, pulse,
and recolor useful without adding graph-specific animation commands.
grid3 and axes3 remain the fastest defaults for a simple positive stage.
Choose frame3 when bounds, log scale, multiple walls, repeated sections, or
textbook/spatial continuity matter.
▶ Solve the 3 × 4 × 12 box in three honest views:
▶ Pure 3D: shortest distance from a point to a plane:
▶ Six Asymptote-inspired grid policies:
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 |
| frame3 | frame3(world,(0,0,0),(8,8,6),"planes=xy:min,xz:min mode=textbook"); | bounded scientific axes plus selected grid planes |
| randomwalk3 | randomwalk3(walk,(0,0,0),10000,21,"mode=turtle angle=60 color=turn shade=depth"); | one deterministic 3D path with a local/world direction model |
| hilbert3 | hilbert3(curve,(0,0,0),6,4); | one exact space-filling path through all cells of a 3D lattice |
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), (18, -14)); // optional screen offset clears the marker
pin3 deliberately stays the same screen size. When the label should feel as
if it lives in the world, use label3 and give its desired world height:
text(pName, (0,0), "P");
label3(pName, pointP, 0.35); // gets smaller as P moves away
Relationships, projections, and contours
These are live construction words, not snapshots:
point3(p, (2,1,3));
project3(shadow, p, "xy");
link3(drop, p, shadow, 0.06);
surface3(bowl, "x^2+y^2", (-2,2), (-2,2), 24);
contour3(levelOne, bowl, 1);
Move p and both shadow and drop recompute every frame. contour3 accepts
a surface3 height field and extracts the requested z level. Use these words
for geometry, vectors, optimization, fields, and engineering callouts—the
engine owns the relationship while the author owns the story.
Deterministic 3D random walks
randomwalk3 keeps tens of thousands of decisions in one path, so draw-on,
camera fitting, direct seeking, and export remain predictable:
camera3((10,-12,8),(0,0,0),38,orthographic);
randomwalk3(walk,(0,0,0),12000,21,
"mode=turtle angle=60 distribution=gaussian color=turn shade=depth scale=0.12");
untraced(walk);
par {
draw(walk,2.4,smooth);
view3(walk,"fit",0.7,smooth,1.2);
}
mode=axischooses one of ±x, ±y, ±z on every step.mode=turtleadvances along a local heading, then turns that orientation frame; useangle=90or60for the classic textbook constructions.distribution=uniform|gaussianchanges how the six choices are sampled.color=direction(orturn) paints segments by the selected choice.shade=depthadds a camera-aware far/near cue without changing the path.scaleis the world-space length of every step.seedmakes comparison and rerendering exact. The bounded maximum is 50,000 steps.
See asymptote-randomwalk3-reference.manic for the recurring Asymptote models
and creator-randomwalk3-diffusion.manic for a 100 → 1,000 → 10,000-step
explanation.
A line that fills a cube
hilbert3 generates one exact 3D Hilbert path as a single seekable entity.
Order 1 visits 8 cells with 7 segments; every refinement replaces each visit
with eight smaller visits, so order 5 has 32,767 segments:
camera3((13,-15,11),(0,0,0),42);
hilbert3(curve,(0,0,0),6,1);
hilbert3(next,(0,0,0),6,2); hidden(next);
untraced(curve);
draw(curve,1.5,smooth);
become3(curve,next,1.3,smooth);
The default arc-length gradient stays continuous through become3; choose
"color=single" when an ordinary color(curve, cyan) should paint the whole
path. Orders are deliberately bounded to 1–5 so direct seeking and production
rendering remain predictable. See
How one line fills a cube for the full
7 → 32,767-segment story with a continuous camera and CTA.
Large evolving 3D stories
Four small foundations cover dependency clouds, articulated mechanisms, time-varying fields, and addressable model parts without exposing frame callbacks.
| Intent | Manic words |
|---|---|
| many stable points | collection3 |
| relationships among them | links3, child3 |
| one dependent articulated chain | chain3, trail3 |
| a changing 3D field and its motion | vectorfield3, advect3 |
| a camera that follows one member | followshot3 |
| named parts inside one OBJ | assembly3 |
| a small offline sound beat | cue |
Here is the reusable field pattern:
camera3((8,-10,6), (0,0,0), 42);
collection3(seeds, (-2,0,0), 48, 1.2, 21, 0.045);
vectorfield3(flow, (0,0,0), 4,
"-y + 0.35*sin(2*pi*p)",
"x + 0.25*cos(2*pi*p)",
"0.25*sin(x+y+2*pi*p)", 5);
child3(hero, seeds, 0, 0.08);
trail3(history, seeds, 0, 0.025);
par {
advect3(seeds, flow, 5, 0.45);
followshot3(hero);
}
followshot3(none);
p is normalized absolute time from 0 to 1. Manic precompiles the RK4 paths,
then samples those paths, the field arrows, the child proxy, the trail, and the
camera target from the same timeline time. Scrubbing backward or jumping
straight to the middle is therefore repeatable.
Use collection3 for a fixed-count repeated 3D cast. It renders as a batch,
while child3 exposes only the member that needs a label or camera. links3
can create chain, nearest, or all relationships without hundreds of
handwritten link3 calls:
collection3(cloud, (0,0,0), 80, 3, 42);
links3(neighborhood, cloud, nearest, 2);
drift3(cloud, 4, 0.6);
For a dependent mechanism, every chain3 endpoint starts where its predecessor
ended. The history is taken from the real compiled endpoint route:
collection3(arm, (0,0,0), 4, 0, 7, 0.07);
chain3(arm, "1.8 1.35 1.0", "1.0 -1.8 2.4", 6);
links3(bones, arm, chain);
ring3(firstOrbit, arm, 0, 72);
ring3(secondOrbit, arm, 1, 72);
trail3(tipHistory, arm, 3, 0.035);
ring3 does not approximate a separately animated circle. Its centre is the
previous endpoint and its radius is the current distance to the chosen child,
so it remains correct while the chain rotates, travels, seeks, or rewinds.
Turn that same member history into a truthful screen-space plot with
historyplot. Choose x, y, or z; Manic uses the complete compiled range
for stable scaling and reveals only the history reached so far:
historyplot(wave, arm, 3, y, (540,1250), (820,320));
Use historyplot3 when the trace belongs inside the 3-D world rather than the
screen overlay. Its origin is a world coordinate and its size is measured in
world units, so depth stacking, focus shots, orbiting, and follow cameras all
remain coherent:
historyplot3(wave3, arm, 3, y, (-3,-4,2), (8,2.5));
view3(wave3, "front", 1, smooth, 1.2);
Both forms are derived from the same compiled child motion. Use historyplot
for a dashboard or fixed teaching panel; use historyplot3 for layered
Fourier constructions, spatial signal galleries, and camera-led explanations.
For a procedural tree, tree3 keeps authoring and rendering bounded. Each
generation is one addressable edge batch, while leaves are one collection:
tree3(tree, (0,0,0), 2.1, 27, 0.72, 10, 42);
for i in 0..10 { untraced(tree.d{i}); }
stagger(0.12) { for i in 0..10 { draw(tree.d{i},0.5,smooth); } }
view3(tree, "isometric", 1.0, smooth, 1.35);
This is the right tradeoff for creator files: a meaningful generated recipe, normal layers/tags and camera verbs, but no recursive user callback.
For grouped assets, OBJ group names become safe part ids:
assembly3(console, "asset:models/manic-console.obj", (0,0,0), 1.4);
text(screenLabel, (0,0), "signal");
label3(screenLabel, console.screen, 0.28);
cue(chime);
This is deliberately bounded: stable-count collections, formula fields, named OBJ groups, camera-facing labels, and four local cues. GLB node/material hierarchies, arbitrary SFX files, shader callbacks, count-changing children, and true occluding 3D glyph meshes remain future layers.
▶ Living dependency cloud:
▶ Dependent chain and truthful history:
▶ Fourier chain to live derived waveform:
▶ Five depth-layered Fourier families (DefinedMotion animation2.gif test):
▶ Odd harmonics to square-wave partial sum:
▶ Batched fractal-tree growth:
▶ Time-varying field and follow shot:
▶ Addressable assembly, notation, cue, and particle punctuation:
Textbook dimension-story series
These portrait stories use the same 3D engine for a textbook-friendly purpose: start with one familiar object, preserve its identity, and let the next dimension arrive through motion. Each gallery page contains the complete source and its video card.
| Story | Dimensional journey | Teaching idea |
|---|---|---|
| The trapped light beam | 1D → 2D → 3D | Distance grows from 5 to 13 to 85 through nested right triangles. |
| How space learned to grow | point → line → plane → room | A line sweeps into a surface; the surface lifts into volume. |
| Length, area, volume | 1D → 2D → 3D | Why measured units become cm, cm², and cm³. |
| A point gets an address | x → (x,y) → (x,y,z) | Each new axis adds one coordinate to the same point. |
| The revolving semicircle | diameter → curve → sphere | A solid can be generated from a lower-dimensional rule. |
| Statistical dimensions | list → scatter → cloud | More variables require a richer coordinate world. |
| Dimension reduction | 3D → 2D → 1D | A sphere reveals a section, then the section reveals a diameter. |
| Watermelon sections | whole → halves → ¼ + ¾ | Perpendicular great-circle cuts create meaningful pieces. |
Creator pattern: keep the mathematical subject persistent, introduce one axis
or section at a time, and hold the settled frame long enough to read the new
formula. Use view3(...,"fit") at dimensional boundaries; use ordinary object
motion inside each explanation.
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.
Make the same generated object change
Use an ordinary parameter, add p to the generated formula, and connect it
with bind. Manic resamples the same object instead of replacing it:
parameter(shape, (640,620), 0, 0, 1, "shape", 2);
surface3(world, "0.22*(x*x+y*y)", (-3,3), (-3,3), 34);
bind(shape, world, formula, "0.22*x*x + 0.22*(1-2*p)*y*y");
to(shape, value, 1, 3, smooth);
The same pattern works for every stable formula family:
bind(shape, helix, formula,
"(1+p)*cos(t)", "(1+p)*sin(t)", "0.3*t");
bind(shape, torus, formula,
"(3+(0.2+p)*cos(v))*cos(u)",
"(3+(0.2+p)*cos(v))*sin(u)",
"(0.2+p)*sin(v)");
This is a useful creator distinction:
- use
bindwhen one curve/surface remains the subject and its mathematical rule changes continuously; - use
morph3when one authored shape becomes a different authored shape.
The bound object keeps its id, resolution, colour, material, transform, and
timeline identity. Manic checks the declared parameter range at build time and
rejects sampled non-finite formulas before recording. Topology and item count
stay fixed, so this is not a per-vertex scripting API. Do not combine morph3
and a generated-family binding on the same target. gradient3,
tangentplane3, and volume3 stay attached to that source and resample as it
deforms, so the measurement and the surface cannot drift apart.
▶ General three-family reference:
▶ Creator Short — solve the bowl-to-saddle transition:
Design tip: author the constructor formula to match the parameter’s initial value. Then the first reveal and the live journey share the same exact shape, with no opening jump.
Textbook sections: halves, quarters, and the remainder
param3 is also the V2 route for an exact authored section of a curved solid.
Bound one parameter to describe only the required half or quarter of the sphere,
and add a second bounded surface for the exposed cut face. This keeps the lesson
mathematical: the geometry is defined by the section, not hidden by a flat mask.
The watermelon example uses that pattern to compare horizontal and vertical great-circle cuts, then makes two perpendicular cuts and separates a quarter from its three-quarter remainder:
// A sphere quarter: longitude u spans 90°, latitude v spans the full height.
param3(quarter,
"3*cos(v)*cos(u)", "3*cos(v)*sin(u)", "3*sin(v)",
(0, pi/2), (-pi/2, pi/2));
finish3(quarter, "shading=smooth material=matte depth=0.25 shadow=0.18");
▶ Animated textbook reconstruction:
Open the complete, copyable source in the 3D scenes gallery.
Practical authoring tips:
- Keep the outside shell and each exposed section face as separate tagged entities. They can be revealed, shifted, and recoloured as one teaching unit.
- Use a light
papertemplate and restrained face colours when the goal is a textbook diagram; depth should clarify the construction, not dominate it. - Frame each new construction with
view3(tag,"fit",...)after the cast changes. The camera then follows the mathematical subject instead of fixed coordinates. - V2 supports exact authored sections through bounded
param3. A generic verb that cuts any arbitrary solid and automatically creates the resulting pieces is intentionally deferred to V3.
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);
For a horn, vessel, nerve, pipe, or any path whose radius changes, use a
normalized radius profile (t=0 start, t=1 end):
curve3(spine, "4*t-2", "sin(6*t)*0.2", "0", (0,1));
tube3(horn, spine, "0.06 + 0.28*t", 14);
One optional render finish
The default is still Manic’s restrained, template-aware diagram rendering.
When an object needs a different surface treatment, finish3 keeps the choice
in one bounded string:
finish3(globe, "shading=smooth material=metal depth=0.2 shadow=0.2");
finish3(terrain, "mesh=0.25 texture=checker scale=3");
finish3(shell, "material=glass shading=smooth");
- Start with
shading=smoothfor spheres/organic surfaces and leave boxes flat. - A little
meshclarifies topology;1is intentionally strong. depthandshadoware subtle readability controls from0to1, not a replacement for authored lighting.checkerandstripesare procedural and deterministic; Manic does not load texture scripts or arbitrary shaders.
Controlled OBJ models
model3(mark, "asset:models/manic-pyramid.obj", (0,0,1), 1.4);
finish3(mark, "shading=smooth material=metal mesh=0.12");
assembly3(console, "asset:models/manic-console.obj", (3,0,0), 1.2);
show(console.screen, 0.4);
model3 reads geometry only: OBJ vertices, polygon faces (triangulated), and
lines. It ignores material/script features and enforces file/geometry limits.
An asset: URI selects a file packaged with Manic, so it works from the CLI,
Docker image, or production backend without a launch-directory assumption or
extra flag. An ordinary path such as uploads/my-model.obj still works for a
user-owned model, but the UI/backend must provision that file. Use built-in
solids when they express the same idea—they remain the lightest option.
Available bundled 3D assets
| Stable URI | What it is | Good for |
|---|---|---|
asset:models/manic-pyramid.obj | Small generic pyramid OBJ | Learning model3, a beacon, marker, monument, or placeholder model |
asset:models/manic-console.obj | Grouped console OBJ (base, screen, key) | Learning assembly3, part callouts, staged reveals, and technical product stories |
Bundled names are intentionally few and predictable. See the full
Bundled assets catalog and do not invent an asset: name that is
not listed. To add one to Manic itself, place the
geometry-only file under assets/models/, document its URI here and in
assets/README.md, and add a checked example. The release, Docker, EC2, and
playground pipelines copy the complete assets/ directory automatically, so
future catalog entries need no per-file deployment rule.
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
These remain the precise controls. Use them when exact coordinates or a specific orbit are part of the explanation; use the creator-first five for camera composition and relationship choreography.
Check the transitions, not only the last frame
manic check examples/three-d-v2-lab.manic --canvas portrait
The publishing audit samples camera transitions between named steps. It warns
when projected 3D bounds leave the creator media rectangle, an orbit/zoom reads
as a shock, the eye enters geometry, or a live spatial relationship has lost
its source. It also checks the settled frame of every step. The most useful
repair is usually semantic: tag the shot’s subject and call view3(tag,"fit")
after the cast changes, or give a camera beat more duration.
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.
Shaders — per-pixel worlds
Most shapes in manic are things — a circle, a line, a label you name and move. A shader is different: it colours every pixel from a formula. No cast, no objects — just maths evaluated once per pixel, every frame. It’s how you get plasmas, fractals, fields, ray-marched 3D, and the whole “generative” look.
manic gives you two paths to this, and they meet in the middle:
The DSL — shader / raymarch | Raw GLSL — glsl | |
|---|---|---|
| you write | manic formulas (let r = …) | a real GLSL fragment shader |
| best for | fields, SDF art, ray-marched scenes you want in the scene | pasting an existing Shadertoy, or exotic multi-pass tricks |
| integrates with | parameters, camera3, the timeline, compositing, the editor’s autocomplete/checks | the universal iTime/iResolution/iMouse (plus opt-in uniforms) |
| runs on | GPU and a deterministic CPU fallback | GPU (Metal / llvmpipe / WebGL) |
Rule of thumb: reach for the DSL when the shader should be part of your animated
scene (driven by a slider, orbited by a camera, composited over other shapes,
recorded deterministically). Reach for glsl() when you already have the shader,
or when you need something the DSL deliberately doesn’t have (loops with feedback,
textures, multi-pass).
Both are first-class scene citizens — you show, fade, and time them like any
entity.
Path 1 — shader: a 2-D colour field
The per-pixel twin of cloud. Each pixel’s colour is a closed-form function of its
normalised coordinates u/v (0..1), aspect asp (width/height), and live
time t:
canvas("16:9");
shader(bg) {
let x = (u - 0.5) * asp; // centre + aspect-correct
let y = v - 0.5;
let d = length(vec2(x, y)); // distance from the middle
let hue = mod(d * 300.0 - t * 40.0, 360.0);
let sat = 0.7;
let val = 0.6;
}
Output one of:
r/g/b— each 0..1 (RGB),hue(degrees) + optionalsat/val(HSL — great withatan2),- a lone
c— greyscale, - and optionally
let alpha(0..1) so the field is transparent and composites over the scene behind it.
Everything else you write is an intermediate let. Fill the canvas, or render into
a panel: shader(id, (cx,cy), w, h) { … }.
⚠️
u/vare 0..1 on both axes, so a rawlength(u-0.5, v-0.5)is an ellipse on a wide canvas. Correct withasp(let x = (u-0.5)*asp) as above.
Draw with distance — the SDF shape library
You rarely need to derive a shape by hand. manic ships signed-distance builtins — negative inside the shape, positive outside — that you band, fill, outline, or combine:
shader(art) {
let x = u * asp;
let y = v;
// three shapes, unioned (smin ≈ min; there's no callable `min` in a formula)
let d = smin(smin(
sdhexagon(x - 0.6, y - 0.5, 0.15),
sdstar(x - 1.05, y - 0.5, 0.17, 5), 0.01),
sdcircle(x - 1.5, y - 0.5, 0.13), 0.01);
let fl = fill(d, 0.0, 0.004); // solid inside
let ol = stroke(d, 0.0, 0.03, 0.004); // bright outline
let r = mix(0.1, 0.16, fl) + ol;
let g = mix(0.1, 0.52, fl) + ol;
let b = mix(0.2, 0.62, fl) + ol;
}
2-D shapes (all of centred x,y): sdcircle(x,y,r), sdtriangle(x,y,r),
sdhexagon(x,y,r), sdpolygon(x,y,r,n) (pentagon/octagon/… by side count),
sdstar(x,y,r,n), sdrhombus(x,y,rx,ry), sdvesica(x,y,r,d),
sdroundbox(x,y,w,h,r).
Combine: smin(a,b,k) (smooth union — also a plain union with tiny k),
sdsub(a,b) (carve), sdint(a,b) (intersect), sdround(d,r) (grow), sdonion(d,r)
(shell). Tile: rep(x,r) (infinite grid), rand2(i,j) (per-cell hash).
Shaping, colour & easing helpers
Scalar helpers usable in any formula: map(v,iMin,iMax[,oMin,oMax]), saturate,
within, select(a,b,c) (branchless), gain/parabola/gaussian/bump
(iq curves), smootherstep/quintic/cubic, mirror (triangle wave),
decimate (quantise). Colour: contrast(v,k), blends blendscreen/blendoverlay/
blendsoftlight(a,b), gamma2linear/linear2gamma. Springy easings of a 0..1
value: elasticout(t), bounceout(t), backout(t). Plus the GLSL idioms
(mix, smoothstep, clamp, fract, step, atan2) and the escape-hatch
generators voronoi(x,y), mandelbrot(x,y), julia(zx,zy,cx,cy).
Vector maths — custom SDFs & domain warps
For anything a builtin doesn’t cover — a fold you invent, a reflection, or
warping space itself — the DSL has real vectors (in shader/raymarch only):
shader(warp) {
let p = vec2(u * asp - 0.9, v - 0.5); // name the coordinate as a vec2
let ang = length(p) * 7.0 - t; // twist grows with radius
let q = rot2(p, ang); // rotate SPACE, reuse the vec2
let hue = mod(200.0 + q.x * 500.0 + q.y * 300.0, 360.0);
let sat = 0.8;
let val = 0.5 + 0.4 * sin(q.y * 40.0);
}
Build vec2(a,b) / vec3(a,b,c), combine component-wise (+ − * /, scalars
broadcast), read parts with .x / .y / .z, and use dot, cross, length,
normalize, distance, reflect, and rot2(v, angle). A let can be a vector
(let p = vec2(...)) — name it once, reuse it. (A shader’s final r/g/b/etc.
must still be a number.)
Driven by a slider
Reference any scene parameter by name — the field re-renders as it animates,
not only with time:
parameter(freq, (640, 660), 3, 1, 14, "freq", 0);
shader(rings) {
let d = hypot((u - 0.5) * asp, v - 0.5);
let hue = mod(d * freq * 90.0, 360.0); // ← the slider drives the pattern
let sat = 0.8;
let val = 0.55;
}
to(freq, value, 14, 6, smooth);
Path 1 (cont.) — raymarch: a ray-marched 3-D scene
Write only the signed-distance field let d (the distance from any point
x/y/z at time t to your scene). The engine marches a ray per pixel, finds
the surface, computes the normal, and shades it — no loop, no vectors required:
canvas("16:9");
template("black");
camera3((3.2, -3.6, 2.4), (0, 0, 0), 40, perspective);
raymarch(scene) {
let tor = sdtorus(x, y, z, 0.95, 0.26); // ring in the XY plane
let oct = sdoctahedron(x, y, z - 1.05, 0.5); // floating above
let d = smin(tor, oct, 0.18); // smooth-union them
// optional hit colour, from the surface normal nx/ny/nz + height hz + t
let hue = mod(190.0 + nz * 70.0 + t * 30.0, 360.0);
let sat = 0.82;
let val = 0.52 + 0.32 * nz;
}
orbit3(70, 0, 5.4, 20, smooth); // the marcher re-reads camera3 every frame
3-D primitives (Z is the main axis): sdsphere(x,y,z,r),
sdbox3(x,y,z,bx,by,bz), sdtorus(x,y,z,R,r), sdcylinder(x,y,z,h,r),
sdcapsule(x,y,z,h,r), sdoctahedron(x,y,z,r), sdplane(x,y,z,h) — combined with
the same smin/sdsub/sdint. Add let r/g/b or let hue(+sat/val) for
your own colour, and let alpha to make missed rays transparent so the object
composites over the scene. The camera comes from camera3, so orbit3 sweeps
it. And you can use the vec3 maths above for custom operators.
Path 2 — glsl: run a real GLSL shader
Already have a shader? Paste it. glsl(id, …) hands a Shadertoy-style
mainImage straight to the graphics pipeline — unchanged — at full resolution:
canvas("16:9");
glsl(bg, `
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0, 2, 4));
fragColor = vec4(col, 1.0);
}
`);
wait(6);
You get iTime, iResolution, and iMouse for free. GLSL ES 2.0 rules apply
(loops need constant bounds — fine for a fixed-step ray-march). The same shader
runs on Metal (Mac), llvmpipe (headless/servers), and WebGL (browser).
A raw paste can still opt into the scene: declare uniform float u_<name>; and it
auto-binds to the scene parameter <name>; declare the camera basis
uniform vec3 iCamEye; (+ iCamFwd/iCamRight/iCamUp/iCamThf) and it binds to
camera3, so orbit3 sweeps your raw shader too.
Which path?
- A field, an SDF gallery, a ray-marched object you want animated in your scene →
the DSL (
shader/raymarch). You get parameters,camera3, the timeline, alpha compositing, deterministic recording, and editor autocomplete/checks. - An existing GLSL shader, or a multi-pass/texture/feedback effect →
glsl(). It runs as-is; the DSL doesn’t try to be a full GLSL. - Not sure? Start in the DSL. If you hit a wall,
glsl()is always there as the escape hatch — the two share one pipeline, so nothing you learn is wasted.
Reproducible exports
The DSL path has a deterministic CPU fallback, so manic FILE --cpu-shaders
re-renders shader/raymarch scenes byte-for-byte (GPU output can vary by a few
least-significant bits across drivers). Handy for byte-exact export jobs. Raw
glsl() is GPU-only and unaffected.
Porting a Shadertoy — two things to know
Real Shadertoys mostly run unchanged, but two mismatches are worth knowing up front:
- No input channels.
glsl()suppliesiTime/iResolution/iMouse(and theiCam*basis withcamera3) — but noiChannel0..3textures/audio/video/buffers. A paste that readstexture(iChannel0, …)won’t compile. When the channel was only a noise source, swap it for a procedural hash:texture(iChannel0, vec2(k)).x→fract(sin(k*91.7)*43758.5453). - Don’t shadow a builtin. A variable named
mix,step,length, … shadows the GLSL function of the same name; strict compilers reject it. Rename the variable.
fragCoord already matches Shadertoy’s bottom-left origin, so up/down is correct out of
the box. With those two caveats, classics like TDM’s Seascape run byte-for-byte.
Power move — a shader hosting a lesson
Because a glsl() shader is an ordinary, low-z scene entity, the entire teaching
layer composites on top of it — plot, tangent, slope, deriv, extrema,
inflections, equation (LaTeX), counters, captions. So a real Shadertoy can be the
living stage for a rigorous lesson, with the shader animating underneath the whole time.
examples/glsl-derivative-wave.manic — “manic meets Shadertoy” — runs TDM’s raymarched
ocean while a full Calculus-1 lesson plays over it: the derivative as the slope of a wave
(zero at each crest), then the second derivative for concavity, the second-derivative
test (max vs. min), and inflection points — the whole sin → cos → −sin ladder, on a
living sea.
That’s the point of the shared pipeline: the shader isn’t a wallpaper you switch to — it’s one more entity in the same scene as your maths, your 3-D, and your timeline.
Coming from Manim to Manic
If you’ve written Manim scenes, you already think the right way: a scene is a sequence of things that appear, move, and change over time. Manic keeps that mental model and drops most of the plumbing. This page is a translation guide — what your favourite Manim moves look like in Manic, and where the two languages genuinely differ.
Every scene mentioned here is a runnable file in the gallery under Coming from Manim. Read this page for the why; open those for the how.
Manic is not “Manim with different syntax.” It’s a small declarative language built for people who aren’t programmers. Where Manim hands you Python objects and asks you to wire them together, Manic gives you a builtin that already knows the intent. Most reactive rigs — a
ValueTrackerfeeding an updater feeding analways_redraw— collapse to a single verb. The examples below are the same tutorial scenes you already know, written the Manic way.
The three big differences
1. No ValueTracker, no add_updater, no always_redraw.
This is the headline. In Manim, “keep this line connected to those two dots as
they move” is a hand-written updater. In Manic, a segment between two points is
reactive by construction — it reflows to its endpoints every frame because the
constraint pass runs for you. You rarely write the reactive glue; you state the
relationship once and animate the inputs.
point(a, (200, 400)); point(b, (600, 300));
segment(l, a, b); // stays connected — no updater
slidex(a, 840, 2); // move one dot; the line follows
2. The timeline is deterministic and scrubbable.
Animations in Manic resolve to a pure function of time t. A motion trail
(trail) isn’t accumulated frame-by-frame like a Manim updater that appends
points — it’s sampled from the resolved timeline, so scrubbing backwards shows
exactly the trail up to that moment. You get correct reverse playback for free.
3. You say what happens, not how to redraw it.
Manim’s self.play(Create(x)), Transform, Restore, MoveAlongPath are
render instructions. Manic’s verbs — show, draw, move, to, turn,
grow, fade — are intentions, sequenced with wait and grouped with par { }.
The translation table
| Manim | Manic | Notes |
|---|---|---|
Axes(x_range, y_range, tips=False) | coords(id, origin, xrange, yrange, sx, sy, 0) | Arbitrary ranges, per-axis scale, tips/ticks/auto-numbers, and axis names — one call. Align a plot to the same origin+sx+sy. |
ax.plot(lambda x: 4*x - x**2) | plot(id, origin, sx, sy, "4*x - x*x", (0,4)) | Formula strings; x*x or x^2. |
| custom / non-numeric tick labels | xtick(id, coords, value, [text]) / ytick(…) | Label a tick at an axis value — a number, a word ("Mon"), or a symbol ("π/2"). |
ax.get_vertical_line(ax.i2gp(2, curve)) | vline(id, curve, x, [color]) | Lands exactly on the graph; dotted/dashed/solid. |
ax.get_riemann_rectangles(curve, …, dx) | riemann(id, curve, a, b, [dx], [color]) | The discrete analogue of area. |
ax.get_area(g, [a,b], bounded_graph=…) | band(top, bottom, [color], (a,b)) | Fills only the x-slice between two curves. |
ax.get_graph_label(graph, "\sin x", …) | graphlabel(id, curve, `\sin x` , x, up) | LaTeX label pinned to the curve, coloured to match. |
Dot + add_updater(move_to(c2p(t, f(t)))) | mark(id, curve, x) then to(id, x, …) | A dot that rides a curve — no updater. |
always_redraw(Rectangle origin→(t, k/t)) | boxto(id, curve, x) | Reactive inscribed rectangle; the constant-area demo in one line. |
MathTex("a","+","b") (indexable) | mathparts(id, (x,y), `a` , `+` , `b` ) | Auto-laid-out, baseline-aligned, addressable parts {id}.0, {id}.1, … |
Circle() / Square() (a stroke-first ring) | circle(id, (x,y), r); outlined(id) | outlined drops the fill → a ring; filled reverts. Manic’s default is a filled disc, so opt into the ring with outlined. |
circle.set_fill(PINK, opacity=0.5) | color(id, pink); opacity(id, 0.5, fill) | color sets the fill colour; the fill layer word gives it its own opacity, independent of the rim. |
circle.set_stroke(BLUE, width=4, opacity=0.6) | outline(id, blue); stroke(id, 4); opacity(id, 0.6, stroke) | The stroke/outline layer word gives the rim its own opacity. On a hollow shape, colour the ring with outline, not color. |
self.play(Create(circle)) | untraced(id); draw(id) | The shape draws itself on — the outline traces while the (translucent) fill washes in behind it. Same base-setter+verb idiom as hidden+show. See examples/manim-vs/first-class-shapes.manic. |
Write(text) | untraced(e); draw(e) | A left-to-right write-on wipe. |
SurroundingRectangle(term, buff=.1) | framebox(id, target, [buff]) | Outlined box fitted to a target’s bbox + pad. |
ReplacementTransform(box1, box2) | surround(box, newTarget) | Glides + resizes the box to a new target — one verb, no second box. |
SpiralIn | spiralin(group) | Members spiral into place from a rotated, shrunken offset, staggered. |
mob.save_state() / Restore(mob) | savestate(id) then restore(id) | Snapshot a transform, animate back to it later. |
CyclicReplace / Swap | cycle(a, b, c, …, [dur], [arc°]) | Each entity moves to the next’s position along an arc (default 90°). |
Clockwise / CounterclockwiseTransform | cycle(…, arc°) (signed arc) | Points travel an oriented arc, set by the arc-degrees argument. |
Line(...).rotate(theta) about a vertex | turn(point, vertex, degrees, dur) | Rotate a point about a pivot; attached arms/arcs follow. |
Angle(l1, l2) + add_updater | anglemark(id, a, b, c, ["θ"]) | Reactive arc; optional label rides the bisector as the angle opens. |
Indicate / Flash | pulse(id) / flash(id) | Attention: a scale bump / a radial burst. |
Wiggle | wiggle(id) | Scale-pulse + oscillating rotation — the rotate cousin of shake. |
Circumscribe / CircleIndicate | circumscribe(id, [color]) | A temporary outlined box drawn round the target, then faded — works on a group (tag) too. |
FocusOn | spotlight(id) | A translucent disc converges onto the target, then fades. |
Blink | blink(id) | Blink out-and-in twice. |
ShowPassingFlash / FlashAround | passflash(id, [color]) | A luminous sliver travels the entity’s outline (closed shapes → light circles it). |
x = ValueTracker(0) + set_x updater | slidex(id, x, dur) / slidey(id, y, dur) | Move one coordinate to an absolute value, keeping the other. |
VGroup(...).scale(1.4) | groupscale(tag, 1.4) | Scales about the group’s collective centroid. |
group.animate.shift(dest − group[i]) | dock(tag, member, target) | Slides the group so one member lands on the target — no delta maths. |
VMobject + append-point updater (trace) | trail(id, target, color, width) | Exact, seekable motion trail sampled from the timeline. |
self.camera.frame.animate.move_to(dot) | followshot(id) | The 2D camera tracks an entity; followshot(none) releases. |
frame.animate.scale(0.5) | zoom(2, …) | ⚠️ Inverted: Manim’s smaller frame = Manic’s zoom > 1. |
ZoomedScene + activate_zooming() | loupe(id, (sx,sy), w, h, (dx,dy), mag) | A live magnifier: a frame over the scene + a panel showing that region enlarged. |
Homotopy / ApplyWave / ComplexHomotopy | deform(id, "u(x,y,t)", "v(x,y,t)") | Continuous homotopy — remap a shape’s outline points by (u,v,t) each frame; the animated twin of warp. Write t=0 as the identity (u=x). A wave: "y + 30*sin(x*0.04 + t*tau)*sin(pi*t)". |
self.play(a, b) (parallel) | par { a; b; } | Run verbs together. |
self.wait(t) | wait(t) | Same. |
self.play(FadeIn/FadeOut) | show(id) / fade(id) | See the note on hidden below. |
GrowFromCenter / GrowFromEdge / GrowArrow / GrowFromPoint | hidden(id, from) then show(id) | The reveal style lives on hidden (like untraced arms draw): from = center | up/down/left/right (a bbox edge) | tail (an arrow’s start) | a point (x,y). Scale-in from that anchor; omit from → fade-in. |
Two things that trip people up
hidden is a base state, not an animation. hidden(x) means “x starts
invisible from t=0” — it’s declared once, up top, alongside the constructors.
To hide something mid-scene, use the timeline verb fade(x). (Symmetrically,
show(x) reveals it.) Constructors run at build time, so anything you don’t want
on screen at the start should be declared and hidden before the timeline begins.
zoom is inverted from Manim’s camera scale. Manim shrinks the camera frame
to magnify (frame.scale(0.5)); Manic zooms the view directly, so zoom(2)
magnifies and zoom(1) is the wide shot.
Porting a scene, step by step
A comfortable way to move a Manim scene across:
- Set the frame. Replace the whole
Axes(...)block with onecoords(...)— ranges, per-axis scale, tips, ticks, numbers, and axis names all live there. Point yourplots at the sameorigin,sx,syand they line up. - Delete the reactive rig. Every
ValueTracker,add_updater, andalways_redrawhas a direct verb (slidex,mark,boxto,anglemark,segment,trail, …). Find the builtin that already encodes the relationship instead of re-wiring it — the table above is your lookup. - Sequence with
waitandpar.self.play(a, b)becomespar { a; b; };self.wait(t)becomeswait(t). Reveals areshow/draw; hides arefade. - Declare-and-
hiddenup top. Anything that shouldn’t be on screen at the start is constructed andhiddenbefore the timeline, thenshown when its moment comes.
If a Manim move isn’t in the table, check the API reference — Manic’s vocabulary is broad, and there’s usually a builtin that says it in one line.
Beyond the core: domain kits
Here’s the biggest difference the table above doesn’t show. Manim is a general
animation library: for a neural network you hand-build a NetworkMobject out of
Circles and Lines and fake the activations; for a slider you subclass
NumberLine; for a gradient surface you wire up a Surface and animate the
vectors yourself. (That is literally how the 3Blue1Brown neural-network,
high-dimensions, and gradient-descent videos are made.)
Manic ships kits that compute the object for you — the maths is real, not
drawn — and because every kit object is an ordinary tagged entity, the core
rhetoric (framebox, brace, arrow, say, equation, pulse) dresses it
directly.
| You’d hand-build in Manim | A Manic kit gives you |
|---|---|
a NetworkMobject of circles + edges, activations faked | network + forward / loss / backward / update — a real feed-forward net with computed activations and gradients (large layers auto-abbreviate with ⋮) |
an image Mobject + hand-wired lines to the input neurons | feed(net, digit) — flattens a tensor/digit into the inputs and traces the lit strokes in |
a Surface + hand-animated gradient arrows | surface3 + slice3 (the partial-derivative cross-section) + gradient3 + descend3 (a ball rolling down −∇f, with its trail) |
a rack of NumberLines for a high-dimensional point | sliders + setsliders — N coordinate dials with a live Σxᵢ² |
| a physics sim animated by hand | the physics kit’s integrated sims (spring, pendulum, orbits, …) run on a real solver |
Because they are just entities, you mix core and kit freely — this is the whole point of the language:
network(net, (600, 360), "4 6 3", "relu softmax");
framebox(fb, net.hidden, 14); // core box around a whole KIT layer (a group)
brace(br, (720, 165), (720, 555), 26, right);
surround(fb, net.output, 1.0); // glide the box across, layer by layer
pulse(net.output); // flash the prediction
The kit computes the object; the core gives you the explanation. See the ML kit, the 3D chapter, and the kits overview.
Where to go next
- Coming from Manim gallery — every scene above, runnable.
- 3Blue1Brown, reproduced — longer-form math lessons built the same way.
- The graphing & calculus kit —
coords,plot,area,riemann, and friends in depth. - Motion graphics — the relationship verbs (
attach,become,travel) that replace updaters.
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://maniclang.com.
- Algorithms & data structures — 6 examples
- Graphs — 5 examples
- Grids — pathfinding & automata — 5 examples
- Calculus & functions — 30 examples
- Linear algebra & tables — 13 examples
- Statistics & probability — 14 examples
- Machine learning — 9 examples
- Physics — simulations — 59 examples
- Chemistry — real molecules — 19 examples
- Circuits — schematics that work — 3 examples
- Optics — light as geometry — 14 examples
- Vectors, fields & coordinates — 5 examples
- Geometry (olympiad) — 11 examples
- Transforms & morphing — 15 examples
- Text & UI — 9 examples
- Generative & recursive — 84 examples
- Creator formats — responsive social video — 32 examples
- Race charts — paste a table, get a race — 5 examples
- Boolean shapes — 1 example
- 3D scenes — 51 examples
- Diagrams — 24 examples
- Coming from Manim — 18 examples
- 3Blue1Brown, reproduced — 14 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);
wrap(cap, 1140); // keep long captions inside the page
// ---- anatomy: what a node is, and why "doubly" ----
say(cap, "each node = [ ·prev | data | next· ] — one value, two pointers");
wait(1.4);
say(cap, "\"doubly\" = every node links BOTH ways — a next pointer and a prev pointer; both ends meet NULL, and head marks the front");
wait(1.9);
// ---- grow it: three inserts, each an O(1) pointer splice ----
say(cap, "append 9 at the tail — set last.next → 9 and 9.prev → last. Two pointer writes.");
insert(dll, 3, "9");
wait(1.6);
say(cap, "splice 6 after 8 — four pointers re-thread (8.next→6, 6.prev→8, 6.next→5, 5.prev→6) and nothing else shifts");
insert(dll, 1, "6");
wait(1.9);
say(cap, "insert 1 right after the head — the very same four-pointer splice, done in O(1)");
insert(dll, 0, "1");
wait(1.8);
// ---- shrink it: three deletes, each a re-thread ----
say(cap, "delete the head — head advances to the next node, whose prev becomes NULL");
remove(dll, 0);
wait(1.6);
say(cap, "delete a middle node — its neighbours bypass it: prev.next and next.prev re-point past it, both ways");
remove(dll, 2);
wait(1.9);
say(cap, "delete the tail — the new last node's next becomes NULL");
remove(dll, 4);
wait(1.7);
// ---- the payoff ----
say(cap, "Doubly linked list: O(1) insert/delete at a known node, and you can walk it both ways — the cost is one extra pointer per node");
wait(2.4);
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);
cubic-graphs
A hand-composed graph-theory plate: five 3-connected simple cubic graphs
(including the 6-spoke wheel from a for loop) drawn on in sequence, with
LaTeX node labels ($u_i$, $v_i$) and typeset names $G_1..G_5$ — a paper-style
figure straight from a theorem statement.
title("Five Exceptional Cubic Graphs");
canvas("16:9");
template("paper");
text(titleA, (cx, 34), "Five 3-connected simple cubic graphs");
text(titleB, (cx, 68), "cycle-cover exceptions: at most $\lceil n/6\rceil$ cycles is not guaranteed");
display(titleA);
bold(titleA);
size(titleA, 30);
size(titleB, 22);
color(titleB, dim);
hidden(titleA);
hidden(titleB);
line(frameTop, (42, 104), (w - 42, 104));
line(frameBottom, (42, h - 26), (w - 42, h - 26));
line(frameLeft, (42, 104), (42, h - 26));
line(frameRight, (w - 42, 104), (w - 42, h - 26));
tag(frameTop, frame);
tag(frameBottom, frame);
tag(frameLeft, frame);
tag(frameRight, frame);
stroke(frame, 3);
color(frame, dim);
opacity(frame, 0.45);
untraced(frame);
dot(g1u1, (180, 310), 5);
dot(g1u2, (320, 310), 5);
dot(g1u3, (370, 210), 5);
dot(g1u4, (250, 125), 5);
dot(g1u5, (130, 210), 5);
dot(g1v1, (220, 265), 5);
dot(g1v2, (280, 265), 5);
dot(g1v3, (305, 225), 5);
dot(g1v4, (250, 185), 5);
dot(g1v5, (195, 225), 5);
tag(g1u1, g1Nodes);
tag(g1u2, g1Nodes);
tag(g1u3, g1Nodes);
tag(g1u4, g1Nodes);
tag(g1u5, g1Nodes);
tag(g1v1, g1Nodes);
tag(g1v2, g1Nodes);
tag(g1v3, g1Nodes);
tag(g1v4, g1Nodes);
tag(g1v5, g1Nodes);
line(g1e1, (180, 310), (320, 310));
line(g1e2, (320, 310), (370, 210));
line(g1e3, (370, 210), (250, 125));
line(g1e4, (250, 125), (130, 210));
line(g1e5, (130, 210), (180, 310));
line(g1e6, (220, 265), (280, 265));
line(g1e7, (280, 265), (305, 225));
line(g1e8, (305, 225), (250, 185));
line(g1e9, (250, 185), (195, 225));
line(g1e10, (195, 225), (220, 265));
line(g1e11, (180, 310), (220, 265));
line(g1e12, (320, 310), (280, 265));
line(g1e13, (370, 210), (305, 225));
line(g1e14, (250, 125), (250, 185));
line(g1e15, (130, 210), (195, 225));
tag(g1e1, g1Edges);
tag(g1e2, g1Edges);
tag(g1e3, g1Edges);
tag(g1e4, g1Edges);
tag(g1e5, g1Edges);
tag(g1e6, g1Edges);
tag(g1e7, g1Edges);
tag(g1e8, g1Edges);
tag(g1e9, g1Edges);
tag(g1e10, g1Edges);
tag(g1e11, g1Edges);
tag(g1e12, g1Edges);
tag(g1e13, g1Edges);
tag(g1e14, g1Edges);
tag(g1e15, g1Edges);
text(g1lu1, (165, 336), "$u_1$");
text(g1lu2, (333, 336), "$u_2$");
text(g1lu3, (393, 214), "$u_3$");
text(g1lu4, (270, 124), "$u_4$");
text(g1lu5, (105, 215), "$u_5$");
text(g1lv1, (198, 262), "$v_1$");
text(g1lv2, (301, 262), "$v_2$");
text(g1lv3, (326, 228), "$v_3$");
text(g1lv4, (272, 181), "$v_4$");
text(g1lv5, (174, 227), "$v_5$");
tag(g1lu1, nodeLabels);
tag(g1lu2, nodeLabels);
tag(g1lu3, nodeLabels);
tag(g1lu4, nodeLabels);
tag(g1lu5, nodeLabels);
tag(g1lv1, nodeLabels);
tag(g1lv2, nodeLabels);
tag(g1lv3, nodeLabels);
tag(g1lv4, nodeLabels);
tag(g1lv5, nodeLabels);
equation(g1Name, (250, 366), "G_1", 30);
tag(g1Name, graphNames);
dot(g2u1, (1030, 120), 5);
dot(g2u2, (1160, 210), 5);
dot(g2u3, (1120, 320), 5);
dot(g2u4, (940, 320), 5);
dot(g2u5, (900, 210), 5);
dot(g2u6, (1030, 185), 5);
dot(g2u7, (1095, 225), 5);
dot(g2u8, (1075, 275), 5);
dot(g2u9, (985, 275), 5);
dot(g2u10, (965, 225), 5);
tag(g2u1, g2Nodes);
tag(g2u2, g2Nodes);
tag(g2u3, g2Nodes);
tag(g2u4, g2Nodes);
tag(g2u5, g2Nodes);
tag(g2u6, g2Nodes);
tag(g2u7, g2Nodes);
tag(g2u8, g2Nodes);
tag(g2u9, g2Nodes);
tag(g2u10, g2Nodes);
line(g2e1, (1030, 120), (1160, 210));
line(g2e2, (1160, 210), (1120, 320));
line(g2e3, (1120, 320), (940, 320));
line(g2e4, (940, 320), (900, 210));
line(g2e5, (900, 210), (1030, 120));
line(g2e6, (1030, 120), (1030, 185));
line(g2e7, (1160, 210), (1095, 225));
line(g2e8, (1120, 320), (1075, 275));
line(g2e9, (940, 320), (985, 275));
line(g2e10, (900, 210), (965, 225));
line(g2e11, (1030, 185), (1075, 275));
line(g2e12, (1075, 275), (965, 225));
line(g2e13, (965, 225), (1095, 225));
line(g2e14, (1095, 225), (985, 275));
line(g2e15, (985, 275), (1030, 185));
tag(g2e1, g2Edges);
tag(g2e2, g2Edges);
tag(g2e3, g2Edges);
tag(g2e4, g2Edges);
tag(g2e5, g2Edges);
tag(g2e6, g2Edges);
tag(g2e7, g2Edges);
tag(g2e8, g2Edges);
tag(g2e9, g2Edges);
tag(g2e10, g2Edges);
tag(g2e11, g2Edges);
tag(g2e12, g2Edges);
tag(g2e13, g2Edges);
tag(g2e14, g2Edges);
tag(g2e15, g2Edges);
text(g2lu1, (1010, 114), "$u_1$");
text(g2lu2, (1184, 214), "$u_2$");
text(g2lu3, (1138, 344), "$u_3$");
text(g2lu4, (918, 344), "$u_4$");
text(g2lu5, (874, 214), "$u_5$");
text(g2lu6, (1051, 181), "$u_6$");
text(g2lu7, (1122, 224), "$u_7$");
text(g2lu8, (1095, 280), "$u_8$");
text(g2lu9, (959, 280), "$u_9$");
text(g2lu10, (934, 224), "$u_{10}$");
tag(g2lu1, nodeLabels);
tag(g2lu2, nodeLabels);
tag(g2lu3, nodeLabels);
tag(g2lu4, nodeLabels);
tag(g2lu5, nodeLabels);
tag(g2lu6, nodeLabels);
tag(g2lu7, nodeLabels);
tag(g2lu8, nodeLabels);
tag(g2lu9, nodeLabels);
tag(g2lu10, nodeLabels);
equation(g2Name, (1030, 366), "G_2", 30);
tag(g2Name, graphNames);
let g3cx = cx;
let g3cy = 398;
let g3r = 105;
dot(g3c, (g3cx, g3cy), 5);
tag(g3c, g3Nodes);
for i in 0..12 {
line(g3rim{i}, (g3cx + g3r * cos(-pi/2 + tau * i/12), g3cy + g3r * sin(-pi/2 + tau * i/12)), (g3cx + g3r * cos(-pi/2 + tau * (i + 1)/12), g3cy + g3r * sin(-pi/2 + tau * (i + 1)/12)));
line(g3spoke{i}, (g3cx, g3cy), (g3cx + g3r * cos(-pi/2 + tau * i/12), g3cy + g3r * sin(-pi/2 + tau * i/12)));
dot(g3u{i}, (g3cx + g3r * cos(-pi/2 + tau * i/12), g3cy + g3r * sin(-pi/2 + tau * i/12)), 5);
tag(g3rim{i}, g3Edges);
tag(g3spoke{i}, g3Edges);
tag(g3u{i}, g3Nodes);
}
text(g3l1, (640, 264), "$u_1$");
text(g3l2, (710, 284), "$u_2$");
text(g3l3, (762, 334), "$u_3$");
text(g3l4, (778, 402), "$u_4$");
text(g3l5, (762, 470), "$u_5$");
text(g3l6, (710, 520), "$u_6$");
text(g3l7, (640, 540), "$u_7$");
text(g3l8, (570, 520), "$u_8$");
text(g3l9, (518, 470), "$u_9$");
text(g3l10, (500, 402), "$u_{10}$");
text(g3l11, (518, 334), "$u_{11}$");
text(g3l12, (570, 284), "$u_{12}$");
tag(g3l1, nodeLabels);
tag(g3l2, nodeLabels);
tag(g3l3, nodeLabels);
tag(g3l4, nodeLabels);
tag(g3l5, nodeLabels);
tag(g3l6, nodeLabels);
tag(g3l7, nodeLabels);
tag(g3l8, nodeLabels);
tag(g3l9, nodeLabels);
tag(g3l10, nodeLabels);
tag(g3l11, nodeLabels);
tag(g3l12, nodeLabels);
equation(g3Name, (640, 612), "G_3", 30);
tag(g3Name, graphNames);
dot(g4u1, (210, 625), 5);
dot(g4u2, (350, 625), 5);
dot(g4u3, (410, 510), 5);
dot(g4u4, (290, 435), 5);
dot(g4v1, (255, 560), 5);
dot(g4v2, (320, 560), 5);
dot(g4v3, (340, 510), 5);
dot(g4v4, (290, 485), 5);
dot(g4v5, (220, 520), 5);
dot(g4w1, (170, 500), 5);
dot(g4w2, (145, 450), 5);
dot(g4w3, (95, 565), 5);
tag(g4u1, g4Nodes);
tag(g4u2, g4Nodes);
tag(g4u3, g4Nodes);
tag(g4u4, g4Nodes);
tag(g4v1, g4Nodes);
tag(g4v2, g4Nodes);
tag(g4v3, g4Nodes);
tag(g4v4, g4Nodes);
tag(g4v5, g4Nodes);
tag(g4w1, g4Nodes);
tag(g4w2, g4Nodes);
tag(g4w3, g4Nodes);
line(g4e1, (95, 565), (210, 625));
line(g4e2, (210, 625), (350, 625));
line(g4e3, (350, 625), (410, 510));
line(g4e4, (410, 510), (290, 435));
line(g4e5, (290, 435), (145, 450));
line(g4e6, (145, 450), (95, 565));
line(g4e7, (145, 450), (170, 500));
line(g4e8, (95, 565), (170, 500));
line(g4e9, (170, 500), (220, 520));
line(g4e10, (255, 560), (320, 560));
line(g4e11, (320, 560), (340, 510));
line(g4e12, (340, 510), (290, 485));
line(g4e13, (290, 485), (220, 520));
line(g4e14, (220, 520), (255, 560));
line(g4e15, (210, 625), (255, 560));
line(g4e16, (350, 625), (320, 560));
line(g4e17, (410, 510), (340, 510));
line(g4e18, (290, 435), (290, 485));
tag(g4e1, g4Edges);
tag(g4e2, g4Edges);
tag(g4e3, g4Edges);
tag(g4e4, g4Edges);
tag(g4e5, g4Edges);
tag(g4e6, g4Edges);
tag(g4e7, g4Edges);
tag(g4e8, g4Edges);
tag(g4e9, g4Edges);
tag(g4e10, g4Edges);
tag(g4e11, g4Edges);
tag(g4e12, g4Edges);
tag(g4e13, g4Edges);
tag(g4e14, g4Edges);
tag(g4e15, g4Edges);
tag(g4e16, g4Edges);
tag(g4e17, g4Edges);
tag(g4e18, g4Edges);
text(g4lu1, (190, 650), "$u_1$");
text(g4lu2, (365, 650), "$u_2$");
text(g4lu3, (435, 514), "$u_3$");
text(g4lu4, (294, 414), "$u_4$");
text(g4lv1, (255, 540), "$v_1$");
text(g4lv2, (335, 544), "$v_2$");
text(g4lv3, (360, 505), "$v_3$");
text(g4lv4, (312, 485), "$v_4$");
text(g4lv5, (224, 500), "$v_5$");
text(g4lw1, (176, 480), "$w_1$");
text(g4lw2, (123, 444), "$w_2$");
text(g4lw3, (72, 568), "$w_3$");
tag(g4lu1, nodeLabels);
tag(g4lu2, nodeLabels);
tag(g4lu3, nodeLabels);
tag(g4lu4, nodeLabels);
tag(g4lv1, nodeLabels);
tag(g4lv2, nodeLabels);
tag(g4lv3, nodeLabels);
tag(g4lv4, nodeLabels);
tag(g4lv5, nodeLabels);
tag(g4lw1, nodeLabels);
tag(g4lw2, nodeLabels);
tag(g4lw3, nodeLabels);
equation(g4Name, (275, 690), "G_4", 30);
tag(g4Name, graphNames);
dot(g5a, (1020, 440), 5);
dot(g5b, (1160, 470), 5);
dot(g5c, (1135, 535), 5);
dot(g5d, (1195, 575), 5);
dot(g5e, (1080, 635), 5);
dot(g5f, (915, 625), 5);
dot(g5g, (880, 525), 5);
dot(g5h, (1020, 505), 5);
dot(g5i, (960, 535), 5);
dot(g5j, (1080, 535), 5);
dot(g5k, (980, 590), 5);
dot(g5l, (1060, 590), 5);
tag(g5a, g5Nodes);
tag(g5b, g5Nodes);
tag(g5c, g5Nodes);
tag(g5d, g5Nodes);
tag(g5e, g5Nodes);
tag(g5f, g5Nodes);
tag(g5g, g5Nodes);
tag(g5h, g5Nodes);
tag(g5i, g5Nodes);
tag(g5j, g5Nodes);
tag(g5k, g5Nodes);
tag(g5l, g5Nodes);
line(g5e1, (1020, 440), (1160, 470));
line(g5e2, (1160, 470), (1195, 575));
line(g5e3, (1195, 575), (1080, 635));
line(g5e4, (1080, 635), (915, 625));
line(g5e5, (915, 625), (880, 525));
line(g5e6, (880, 525), (1020, 440));
line(g5e7, (1160, 470), (1135, 535));
line(g5e8, (1135, 535), (1195, 575));
line(g5e9, (1135, 535), (1080, 535));
line(g5e10, (1020, 440), (1020, 505));
line(g5e11, (880, 525), (960, 535));
line(g5e12, (915, 625), (980, 590));
line(g5e13, (1080, 635), (1060, 590));
line(g5e14, (1020, 505), (980, 590));
line(g5e15, (1020, 505), (1060, 590));
line(g5e16, (960, 535), (1080, 535));
line(g5e17, (960, 535), (1060, 590));
line(g5e18, (1080, 535), (980, 590));
tag(g5e1, g5Edges);
tag(g5e2, g5Edges);
tag(g5e3, g5Edges);
tag(g5e4, g5Edges);
tag(g5e5, g5Edges);
tag(g5e6, g5Edges);
tag(g5e7, g5Edges);
tag(g5e8, g5Edges);
tag(g5e9, g5Edges);
tag(g5e10, g5Edges);
tag(g5e11, g5Edges);
tag(g5e12, g5Edges);
tag(g5e13, g5Edges);
tag(g5e14, g5Edges);
tag(g5e15, g5Edges);
tag(g5e16, g5Edges);
tag(g5e17, g5Edges);
tag(g5e18, g5Edges);
equation(g5Name, (1020, 690), "G_5", 30);
tag(g5Name, graphNames);
tag(g1Edges, allEdges);
tag(g2Edges, allEdges);
tag(g3Edges, allEdges);
tag(g4Edges, allEdges);
tag(g5Edges, allEdges);
tag(g1Nodes, allNodes);
tag(g2Nodes, allNodes);
tag(g3Nodes, allNodes);
tag(g4Nodes, allNodes);
tag(g5Nodes, allNodes);
stroke(allEdges, 3);
color(allEdges, fg);
untraced(allEdges);
color(allNodes, fg);
hidden(allNodes);
size(nodeLabels, 19);
color(nodeLabels, dim);
hidden(nodeLabels);
hidden(graphNames);
show(titleA, 0.5);
show(titleB, 0.5);
draw(frame, 0.7);
par {
seq {
draw(g1Edges, 1.0);
show(g1Nodes, 0.25);
show(g1Name, 0.25);
}
seq {
wait(0.25);
draw(g2Edges, 1.0);
show(g2Nodes, 0.25);
show(g2Name, 0.25);
}
seq {
wait(0.5);
draw(g3Edges, 1.2);
show(g3Nodes, 0.25);
show(g3Name, 0.25);
}
seq {
wait(0.75);
draw(g4Edges, 1.1);
show(g4Nodes, 0.25);
show(g4Name, 0.25);
}
seq {
wait(1.0);
draw(g5Edges, 1.0);
show(g5Nodes, 0.25);
show(g5Name, 0.25);
}
}
show(nodeLabels, 0.7);
recolor(graphNames, gold, 0.5);
pulse(graphNames, 0.8);
wait(1.0);
Grids — pathfinding & automata
A first-class 2-D cell grid — the one primitive under tilemaps, spatial pathfinding (space, not graph’s topology), cellular automata and Wave Function Collapse. Cells address like matrix/table ({id}.r{i}c{j}); seed a maze from a compact # . @ * ASCII string; the pathfinders reuse the algo kit’s exact colour grammar (discovered cyan → current magenta → done lime), and generation pre-simulates at build time then replays with run.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
grid-astar
A* over a seeded ASCII maze: open cells flood by f-score (frontier cyan → current magenta), a live frontier/visited readout counts up, and the shortest route traces out in gold as {id}.path. neighbors picks 4- or 8-connectivity.
title("A* Pathfinding — Search Over Space");
canvas("16:9");
template("neon");
watermark(mark, (w*0.15, h*0.05), "Made With Manic");
// A cell grid seeded from a compact ASCII maze: `#` wall, `.` open, `@` start,
// `*` goal. A* explores the open cells (frontier cyan → current magenta), then
// the shortest route is traced in gold — the spatial sibling of the algo kit's
// graph search, sharing its exact colour grammar.
grid(g, "@ . . . . . . . ; # # # # # # . # ; . . . . . . . . ; . # # # # # # # ; . . . . . . . . ; # # # # # # # *", (cx, cy+20), 8, 6, 78);
neighbors(g, "4");
gridastar(g, (0,0), (7,5), manhattan);
draw(g.path, 1.4);
grid-life
Conway’s Game of Life: a glider seeded with setcell, then evolve pre-simulates six generations at build time (alive = a filled cell, Conway’s B3/S23) and run replays them — the glider walks diagonally across the grid.
title("Conway's Life — A Glider");
canvas("16:9");
template("neon");
watermark(mark, (w*0.15, h*0.05), "Made With Manic");
// A cellular automaton on the same cell grid. `step` pre-simulates one Conway
// generation at build time (alive = a filled `wall` cell, 8-neighbourhood);
// `run` then replays the stored generations. This glider walks diagonally.
grid(life, (cx, cy+10), 14, 12, 42);
setcell(life, 1, 2, wall);
setcell(life, 2, 3, wall);
setcell(life, 3, 1, wall);
setcell(life, 3, 2, wall);
setcell(life, 3, 3, wall);
evolve(life, "life"); evolve(life, "life"); evolve(life, "life");
evolve(life, "life"); evolve(life, "life"); evolve(life, "life");
run(life, 6, 5.0);
grid-life-soup
Game of Life at scale: a 40x40 grid seeded with a pseudo-random soup (a shader-style hash, since manic has no rand()), 30 generations evolved then run — the chaos settles into gliders, blinkers and still lifes.
// grid-life-soup — Conway's Game of Life from a random soup, on the grid kit.
//
// A 40x40 cellular automaton seeded with a pseudo-random field (manic has no
// rand(), so a shader-style hash stands in), then evolved: each
// `evolve(life,"life")` pre-simulates one Conway generation at build time
// (alive = a filled `wall` cell, B3/S23, 8-neighbourhood), and `run` replays
// the 30 stored generations — the soup settles into gliders, blinkers and still lifes.
//
// manic examples/grid-life-soup.manic
title("Conway's Life — a random soup");
canvas(1080, 1080);
template("paper");
let cols = 40;
let rows = 40;
let cell = 25;
grid(life, (540, 540), cols, rows, cell);
// seed ~45% of cells at random — `random` is deterministic, so the soup is
// reproducible; render with `--seed random` to roll a fresh soup each time.
for r in 0..rows {
for c in 0..cols {
if random(r, c) > 0.55 {
setcell(life, r, c, wall);
}
}
}
// pre-simulate 30 generations, then replay them over 8s
for g in 0..30 { evolve(life, "life"); }
run(life, 30, 8);
grid-wfc
Wave Function Collapse: collapse pre-simulates a seeded, neighbour-constrained settling row by row, then run replays the grid resolving from empty into a finished maze — deterministic, so the same seed always settles the same way.
title("Wave Function Collapse — A Maze, Settling");
canvas("16:9");
template("neon");
watermark(mark, (w*0.15, h*0.05), "Made With Manic");
// `collapse` pre-simulates a seeded Wave-Function-Collapse-style settling at
// build time — each cell is decided under its neighbours' constraints, one row
// at a time — then `run` replays the grid resolving from empty to a finished
// maze. Seeded, so it is fully deterministic (same seed ⇒ same maze).
grid(mz, (cx, cy+10), 20, 14, 30);
collapse(mz, "maze", 7);
run(mz, 14, 5.0);
grid-life-zoo
Conway’s Life’s whole taxonomy — a still life (Block), an oscillator (Blinker) and a
spaceship (Glider) — three grids seeded with setcell, evolved eight generations and
run in parallel: the Block holds, the Blinker flips, the Glider walks. The famous
pattern zoo, entirely in the grid kit.
// Conway's Life — the Zoo. img_4.png's taxonomy (still lifes / oscillators /
// spaceships) is nothing more than Conway's Life on a grid — which the grid kit
// already does: seed cells, `evolve` a few generations at build time, then `run`
// to replay them. Three grids run in parallel: a Block that never moves, a Blinker
// that oscillates, and a Glider that walks across its grid. No new kit needed.
title("Conway's Life — the Zoo");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
text(kicker, (cx, h*0.10), "STILL LIFE · OSCILLATOR · SPACESHIP — all just Conway's Life");
size(kicker, 22*u); color(kicker, dim); bold(kicker); wrap(kicker, w*0.9);
// --- Still life: Block (never changes) ---
grid(blk, (w*0.22, cy), 6, 6, 40);
setcell(blk, 2, 2, wall); setcell(blk, 2, 3, wall);
setcell(blk, 3, 2, wall); setcell(blk, 3, 3, wall);
text(blkL, (w*0.22, cy+180*u), "Block — still life"); size(blkL, 20*u); color(blkL, cyan);
// --- Oscillator: Blinker (period 2) ---
grid(bln, (w*0.5, cy), 6, 6, 40);
setcell(bln, 2, 1, wall); setcell(bln, 2, 2, wall); setcell(bln, 2, 3, wall);
text(blnL, (w*0.5, cy+180*u), "Blinker — oscillator (p2)"); size(blnL, 20*u); color(blnL, cyan);
// --- Spaceship: Glider (translates) ---
grid(gld, (w*0.78, cy), 8, 8, 34);
setcell(gld, 1, 2, wall);
setcell(gld, 2, 3, wall);
setcell(gld, 3, 1, wall); setcell(gld, 3, 2, wall); setcell(gld, 3, 3, wall);
text(gldL, (w*0.78, cy+180*u), "Glider — spaceship"); size(gldL, 20*u); color(gldL, cyan);
// Pre-simulate 8 generations of each (build time), same count so they replay in sync.
evolve(blk, "life"); evolve(blk, "life"); evolve(blk, "life"); evolve(blk, "life");
evolve(blk, "life"); evolve(blk, "life"); evolve(blk, "life"); evolve(blk, "life");
evolve(bln, "life"); evolve(bln, "life"); evolve(bln, "life"); evolve(bln, "life");
evolve(bln, "life"); evolve(bln, "life"); evolve(bln, "life"); evolve(bln, "life");
evolve(gld, "life"); evolve(gld, "life"); evolve(gld, "life"); evolve(gld, "life");
evolve(gld, "life"); evolve(gld, "life"); evolve(gld, "life"); evolve(gld, "life");
// Replay all three at once — the whole zoo, self-animating.
par {
run(blk, 8, 5.0);
run(bln, 8, 5.0);
run(gld, 8, 5.0);
}
wait(0.6);
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);
calculus-one
Calculus I end to end in one continuous scene: limits (a curve with a hole), the derivative (secants collapsing onto the tangent), curve shape, and the integral + Fundamental Theorem — chapter wipes between.
title("Calculus I, End to End");
canvas("16:9");
template("black");
// ================= HUD (sticky: survives every camera move) =================
text(head, (cx, 60), "Calculus I, End to End"); display(head); cursor(head); sticky(head);
text(h1, (cx, 148), "1. Limits - getting close without arriving"); size(h1, 28); color(h1, cyan); hidden(h1); sticky(h1);
text(h2, (cx, 148), "2. The derivative - a slope caught in the act"); size(h2, 28); color(h2, magenta); hidden(h2); sticky(h2);
text(h3, (cx, 148), "3. What f' knows about the shape of f"); size(h3, 28); color(h3, lime); hidden(h3); sticky(h3);
text(h4, (cx, 148), "4. The integral - area, one strip at a time"); size(h4, 28); color(h4, gold); hidden(h4); sticky(h4);
text(h5, (cx, 148), "5. The Fundamental Theorem - the bridge"); size(h5, 28); hidden(h5); sticky(h5);
text(cap, (cx, h - 42), ""); size(cap, 26); sticky(cap);
caption(menu, "limits - slopes - shapes - areas - the bridge", (cx, cy), 34, gold); hidden(menu);
// chapter wipes, tinted to the incoming chapter
line(w2, (-40, 140), (-40, h - 90)); stroke(w2, 4); color(w2, magenta); glow(w2, 14);
line(w3, (-40, 140), (-40, h - 90)); stroke(w3, 4); color(w3, lime); glow(w3, 14);
line(w4, (-40, 140), (-40, h - 90)); stroke(w4, 4); color(w4, gold); glow(w4, 14);
line(w5, (-40, 140), (-40, h - 90)); stroke(w5, 4); glow(w5, 14);
// ================= ch 1 cast: LIMITS =================
// example A: f(x) = (x^3 - x)/(x - 1) = x(x+1) away from the hole at x = 1
// example B: a limit at infinity - the sigmoid hugging its asymptote
let gx1 = cx + 34;
let gy = 560;
arrow(x1a, (cx - 380, gy), (cx + 390, gy)); untraced(x1a); stroke(x1a, 3); tag(x1a, c1);
arrow(y1a, (gx1, gy), (gx1, 235)); untraced(y1a); stroke(y1a, 3); tag(y1a, c1);
plot(p1, (gx1, gy), 170, 60, "x^2+x", (-2.2, 1.8));
untraced(p1); stroke(p1, 4); gradient(p1, blue, cyan, gold, 270); tag(p1, c1);
dot(rider, (gx1, gy), 7); color(rider, gold); glow(rider, 8); hidden(rider); tag(rider, c1);
limit(lm, p1, 1, gold); hidden(lm); tag(lm, c1);
plot(p1b, (gx1, gy), 170, 60, "4/(1+exp(-2.2*x))", (-2.2, 1.8));
untraced(p1b); stroke(p1b, 4); color(p1b, teal); tag(p1b, c1);
limit(lmb, p1b, inf, cyan); hidden(lmb); tag(lmb, c1);
equation(eq1, (cx, 230), `\lim_{x\to 1}\frac{x^3-x}{x-1}`, 40); hidden(eq1); sticky(eq1);
// ================= ch 2 cast: THE DERIVATIVE =================
// example A: f(x) = x^2/2, secants through (1, 0.5) collapse onto the tangent
// example B: sine's recorded slope is cosine
let gx2 = cx - 170;
arrow(x2a, (cx - 312, gy), (cx + 322, gy)); untraced(x2a); stroke(x2a, 3); tag(x2a, c2);
arrow(y2a, (gx2, gy), (gx2, 240)); untraced(y2a); stroke(y2a, 3); tag(y2a, c2);
plot(p2, (gx2, gy), 170, 80, "0.5*x^2", (-0.6, 2.6));
untraced(p2); stroke(p2, 4); gradient(p2, blue, magenta, 270); tag(p2, c2);
dot(anch, (gx2 + 170, gy - 40), 7); color(anch, gold); glow(anch, 8); hidden(anch); tag(anch, c2);
for k in 0..3 {
let hh = 1.5 - 0.6*k;
let mm = 1 + hh/2;
let ya = 0.5 - 0.9*mm;
let yb = 0.5 + 1.4*mm;
line(sec{k}, (gx2 + 17, gy - ya*80), (gx2 + 408, gy - yb*80));
stroke(sec{k}, 3); color(sec{k}, teal); opacity(sec{k}, 0.45 + 0.2*k); hidden(sec{k}); tag(sec{k}, c2);
dot(sd{k}, (gx2 + (1 + hh)*170, gy - ((1 + hh)^2/2)*80), 6);
color(sd{k}, teal); hidden(sd{k}); tag(sd{k}, c2);
}
tangent(tg2, p2, 1, 280); hidden(tg2); tag(tg2, c2);
slope(sl2, p2, 1); hidden(sl2); tag(sl2, c2);
deriv(dv2, p2, magenta); untraced(dv2); tag(dv2, c2);
line(bl2, (gx2 - 90, 445), (gx2 + 445, 445)); dashed(bl2); stroke(bl2, 2); color(bl2, dim); hidden(bl2); tag(bl2, c2);
plot(p2s, (gx2, 445), 170, 80, "1.1*sin(1.8*x)", (-0.5, 2.6));
untraced(p2s); stroke(p2s, 4); color(p2s, cyan); tag(p2s, c2);
deriv(dvs, p2s, magenta); untraced(dvs); dashed(dvs, 12, 9); tag(dvs, c2);
equation(eq2, (cx, 230), `f'(1) \;=\; \lim_{h\to 0}\frac{f(1+h)-f(1)}{h}`, 38); hidden(eq2); sticky(eq2);
// ================= ch 3 cast: SHAPE =================
// f(x) = x^3/3 - x : turning points at x = -1, 1; concavity flips at 0
// (one horizontal axis only - no vertical line slicing through the curve)
let gy3 = 420;
arrow(x3a, (cx - 436, gy3), (cx + 446, gy3)); untraced(x3a); stroke(x3a, 3); tag(x3a, c3);
plot(p3, (cx, gy3), 180, 150, "x^3/3-x", (-2.2, 2.2));
untraced(p3); stroke(p3, 4); gradient(p3, blue, lime, gold, 270); tag(p3, c3);
tangent(tg3, p3, -2.1, 300); hidden(tg3); tag(tg3, c3);
slope(sl3, p3, -2.1); hidden(sl3); tag(sl3, c3);
extrema(ex3, p3, gold); hidden(ex3); tag(ex3, c3);
inflections(if3, p3, magenta); hidden(if3); tag(if3, c3);
equation(eq3, (cx, 230), `f'(x)=x^2-1`, 38); hidden(eq3); sticky(eq3);
// ================= ch 4 + 5 cast: INTEGRAL and the FTC =================
// example A: f(x) = 0.3 x (4 - x), area 0..4 is exactly 3.2
// example B: a sine arch whose signed area cancels to zero
let gx4 = cx - 340;
arrow(x4a, (gx4 - 40, gy), (gx4 + 730, gy)); untraced(x4a); stroke(x4a, 3); tag(x4a, c4);
arrow(y4a, (gx4, gy), (gx4, 240)); untraced(y4a); stroke(y4a, 3); tag(y4a, c4);
plot(p4, (gx4, gy), 170, 80, "0.3*x*(4-x)", (0, 4));
untraced(p4); stroke(p4, 4); gradient(p4, blue, gold, 270); tag(p4, c4);
for i in 0..8 {
let xm = (i + 0.5)*0.5;
let hgt = 0.3*xm*(4 - xm);
rect(rs{i}, (gx4 + xm*170, gy - hgt*80/2 + 24), 0.5*170 - 3, hgt*80);
color(rs{i}, gold); opacity(rs{i}, 0.55); tag(rs{i}, rstrips);
}
hidden(rstrips);
area(ar4, p4, 0, 0); color(ar4, gold); opacity(ar4, 0.3);
integral(int4, p4, 0, 0, (gx4 + 640, 300)); hidden(int4);
plot(p4b, (gx4, gy), 170, 80, "1.2*sin(1.5708*x)", (0, 4));
untraced(p4b); stroke(p4b, 4); color(p4b, teal);
area(arb, p4b, 0, 0); color(arb, teal); opacity(arb, 0.3);
integral(intb, p4b, 0, 0, (gx4 + 640, 350)); hidden(intb);
equation(eq4, (cx, 230), `A \;\approx\; \sum_i \textcolor{gold}{f(x_i)}\,\Delta x`, 38); hidden(eq4); sticky(eq4);
// FTC: the accumulation A(x) drawn as its own curve, then ITS derivative
accum(acF, p4, 0, gold); untraced(acF);
deriv(dfF, acF, magenta); untraced(dfF); dashed(dfF, 12, 9);
text(alab, (gx4 + 700, 296), "A(x) - area so far"); size(alab, 24); color(alab, gold); hidden(alab);
text(flab, (cx, 430), "f(x)"); size(flab, 24); color(flab, cyan); hidden(flab);
equation(eqF, (cx, 230), `A(x) \;=\; \int_0^x f(t)\,dt`, 40); hidden(eqF); sticky(eqF); z(eqF, 10);
// finale
circle(confbox, (cx, 230), 80); hidden(confbox);
particles(conf, confbox, 22, 4, 9, "ring"); color(conf, gold); hidden(conf);
caption(r1, "zoom in: the derivative is the slope of a moment", (cx, 400), 28, magenta); hidden(r1);
caption(r2, "add up: the integral is the area of a journey", (cx, 455), 28, gold); hidden(r2);
caption(r3, "the Fundamental Theorem: each undoes the other", (cx, 510), 28); hidden(r3);
// ================= timeline =================
// ---- cold open
type(head, 1.2);
wordpop(menu, 0.07);
wait(1.0);
fade(menu, 0.5);
// ---- ch 1: LIMITS ------------------------------------------------------
mark("limits");
cue(whoosh);
show(h1, 0.5);
par { draw(x1a, 0.6); draw(y1a, 0.6); }
show(rider, 0.2);
par { draw(p1, 1.4); travel(rider, p1, 1.4, smooth); }
fade(rider, 0.3);
show(eq1, 0.6);
say(cap, "At x = 1 this formula divides zero by zero - the curve has a HOLE", 0.5);
show(lm, 0.6);
cue(tick);
rewrite(eq1, `\lim_{x\to 1}\frac{x^3-x}{x-1} \;=\; \lim_{x\to 1}\,x(x+1)`, 0.9);
par { cam((gx1 + 170, 470), 1.2, smooth); zoom(1.8, 1.2, smooth); }
say(cap, "The limit asks: where were you HEADING? Watch the dot commit", 0.5);
to(lm, x, 1, 2.6, smooth);
cue(tick);
rewrite(eq1, `\lim_{x\to 1}\frac{x^3-x}{x-1} \;=\; 2`, 0.9);
flash(lm, cyan);
wait(0.8);
par { cam((cx, cy), 1.0, smooth); zoom(1, 1.0, smooth); }
// second example: chasing x to infinity
say(cap, "Limits also ask where the ROAD ends: chase x toward infinity", 0.5);
par { fade(p1, 0.5); fade(lm, 0.5); }
draw(p1b, 1.2);
show(lmb, 0.5);
to(lmb, x, 1.8, 2.0, smooth);
cue(tick);
rewrite(eq1, `\lim_{x\to\infty}\frac{4}{1+e^{-2.2x}} \;=\; 4`, 0.9);
wait(0.7);
par { shift(w2, (w + 80, 0), 0.7, linear); fade(c1, 0.6); disintegrate(eq1, 0.8); fade(h1, 0.5); }
// ---- ch 2: THE DERIVATIVE ----------------------------------------------
mark("derivative");
cue(whoosh);
show(h2, 0.5);
par { draw(x2a, 0.6); draw(y2a, 0.6); }
draw(p2, 1.2);
show(anch, 0.4);
show(eq2, 0.6);
say(cap, "Pin one point. A SECANT through a neighbour measures average slope", 0.5);
par { cam((gx2 + 200, 470), 1.0, smooth); zoom(1.35, 1.0, smooth); }
par { show(sec0, 0.4); show(sd0, 0.4); }
cue(tick);
rewrite(eq2, `\frac{\Delta y}{\Delta x} \;=\; 1+\tfrac{h}{2} \;=\; 1.75`, 0.8);
par { fade(sec0, 0.4); fade(sd0, 0.4); show(sec1, 0.4); show(sd1, 0.4); }
cue(tick);
rewrite(eq2, `\frac{\Delta y}{\Delta x} \;=\; 1+\tfrac{h}{2} \;=\; 1.45`, 0.8);
par { fade(sec1, 0.4); fade(sd1, 0.4); show(sec2, 0.4); show(sd2, 0.4); }
cue(tick);
rewrite(eq2, `\frac{\Delta y}{\Delta x} \;=\; 1+\tfrac{h}{2} \;=\; 1.15`, 0.8);
say(cap, "Slide the neighbour home: the secants collapse onto ONE line", 0.5);
par { fade(sec2, 0.5); fade(sd2, 0.5); show(tg2, 0.5); show(sl2, 0.5); }
flash(tg2, gold);
cue(pop);
cue(tick);
rewrite(eq2, `f'(1) \;=\; 1`, 0.9);
wait(0.6);
par { cam((cx, cy), 1.0, smooth); zoom(1, 1.0, smooth); }
// now do it EVERYWHERE: the tangent glides, recording f' as a new curve
say(cap, "Do it at every point - and record the answer as a NEW curve", 0.5);
par { to(tg2, x, -0.4, 0.7, smooth); to(sl2, x, -0.4, 0.7, smooth); }
par { to(tg2, x, 2.3, 2.6, smooth); to(sl2, x, 2.3, 2.6, smooth); draw(dv2, 2.6); }
cue(tick);
rewrite(eq2, `f'(x) \;=\; x`, 0.9);
wait(0.6);
// second example: every curve carries its shadow
say(cap, "It works for EVERY curve: sine's recorded slope is cosine", 0.5);
par { fade(p2, 0.5); fade(dv2, 0.5); fade(tg2, 0.5); fade(sl2, 0.5); fade(anch, 0.4); }
par { show(bl2, 0.4); draw(p2s, 1.2); }
draw(dvs, 1.6);
cue(tick);
rewrite(eq2, `\big(\sin x\big)' \;=\; \cos x`, 0.9);
wait(0.7);
par { shift(w3, (w + 80, 0), 0.7, linear); fade(c2, 0.6); disintegrate(eq2, 0.8); fade(h2, 0.5); }
// ---- ch 3: SHAPE -------------------------------------------------------
mark("shape");
cue(whoosh);
show(h3, 0.5);
draw(x3a, 0.7);
draw(p3, 1.4);
show(eq3, 0.6);
say(cap, "The derivative is a story about SHAPE. Ride the tangent and listen", 0.5);
par { show(tg3, 0.5); show(sl3, 0.5); }
par { to(tg3, x, -1, 1.6, smooth); to(sl3, x, -1, 1.6, smooth); }
cue(pop);
show(ex3, 0.4); pulse(ex3, 0.8);
say(cap, "Slope zero: the curve turns. A peak - then a valley", 0.5);
par { to(tg3, x, 0, 1.2, smooth); to(sl3, x, 0, 1.2, smooth); }
show(if3, 0.4); flash(if3, magenta);
say(cap, "Here the BEND flips: concave down becomes concave up - an inflection", 0.5);
par { to(tg3, x, 1, 1.2, smooth); to(sl3, x, 1, 1.2, smooth); }
pulse(ex3, 0.8);
par { to(tg3, x, 2.1, 1.2, smooth); to(sl3, x, 2.1, 1.2, smooth); }
cue(tick);
rewrite(eq3, `f'=0:\ \text{turning}\qquad f''=0:\ \text{bending flips}`, 0.9);
wait(1.0);
par { shift(w4, (w + 80, 0), 0.7, linear); fade(c3, 0.6); disintegrate(eq3, 0.8); fade(h3, 0.5); }
// ---- ch 4: THE INTEGRAL ------------------------------------------------
mark("integral");
cue(whoosh);
show(h4, 0.5);
par { draw(x4a, 0.6); draw(y4a, 0.6); }
draw(p4, 1.3);
show(eq4, 0.6);
say(cap, "The other question: how much accumulates? Slice it into strips", 0.5);
stagger(0.08) { for i in 0..8 { par { show(rs{i}, 0.3); shift(rs{i}, (0, -24), 0.4, out); } } }
wait(0.8);
say(cap, "Thinner and thinner strips: the sum becomes the exact area", 0.5);
show(int4, 0.3);
par { fade(rstrips, 1.1); to(ar4, x, 4, 2.2, smooth); to(int4, x, 4, 2.2, smooth); }
flash(int4, gold);
cue(tick);
rewrite(eq4, `A \;=\; \int_0^4 f\,dx \;=\; 3.2`, 0.9);
wait(0.8);
// second example: signed area - the hill cancels the valley
say(cap, "One warning: below the axis, area counts NEGATIVE", 0.5);
par { fade(p4, 0.5); fade(ar4, 0.5); fade(int4, 0.5); }
draw(p4b, 1.2);
show(intb, 0.3);
par { to(arb, x, 4, 2.0, smooth); to(intb, x, 4, 2.0, smooth); }
flash(intb, teal);
cue(tick);
rewrite(eq4, `\int_0^4 \sin\!\Big(\frac{\pi x}{2}\Big)dx \;=\; 0`, 0.9);
say(cap, "The hill above cancels the valley below - signed area", 0.5);
wait(0.6);
par { fade(p4b, 0.5); fade(arb, 0.5); fade(intb, 0.4); show(p4, 0.5); }
par { shift(w5, (w + 80, 0), 0.7, linear); disintegrate(eq4, 0.8); fade(h4, 0.5); }
// ---- ch 5: THE FUNDAMENTAL THEOREM --------------------------------------
mark("ftc");
cue(whoosh);
show(h5, 0.5);
say(cap, "Rewind. This time, GRAPH the running total as you sweep", 0.5);
show(ar4, 0.4);
to(ar4, x, 0, 0.7, smooth);
show(eqF, 0.7);
par { to(ar4, x, 4, 2.8, smooth); draw(acF, 2.8); }
show(alab, 0.5); show(flab, 0.5);
say(cap, "Now take the DERIVATIVE of that gold curve - the slope of the total", 0.5);
cue(tick);
rewrite(eqF, `\frac{d}{dx}A(x) \;=\; \;?`, 0.8);
draw(dfF, 2.2);
flash(p4, magenta);
cue(chime);
say(cap, "It lands EXACTLY on f. Accumulating, then differentiating, is a round trip", 0.5);
cue(tick);
rewrite(eqF, `\boxed{\;\frac{d}{dx}\int_0^x f(t)\,dt \;=\; f(x)\;}`, 1.0);
wait(1.2);
// ---- finale
par { fade(c4, 0.7); fade(ar4, 0.7); fade(acF, 0.7); fade(dfF, 0.7); fade(alab, 0.5); fade(flab, 0.5); fade(h5, 0.5); }
seq { show(conf, 0.1); burst(conf, 1.4); }
par {
breathe(eqF, 3, 0.05, 0, 7);
seq { stagger(0.4) { wordpop(r1, 0.05); wordpop(r2, 0.05); wordpop(r3, 0.05); } }
}
say(cap, "Calculus I: zoom in, add up - and the bridge between", 0.6);
wait(2.5);
integrals
Three views of the integral on one bump: coarse Riemann strips rise into place with the sample dots f(x*) riding the curve, refine to 32, then the true area sweeps open — glowing act-wipes between.
title("Ways to Integrate");
canvas(1000, 1000);
template("black");
// ---------- layout constants ----------
let gx = cx - 340; // graph origin (math x = 0)
let gy = h - 160; // graph baseline (math y = 0)
let sx = 170; // px per x-unit
let sy = 100; // px per y-unit
// ---------- HUD: headings & captions (sticky = survive camera moves) ----------
text(head, (cx, 60), "Ways to Integrate"); display(head); cursor(head); sticky(head);
text(t1, (cx, 122), "1. Riemann - slice the domain"); size(t1, 30); color(t1, cyan); hidden(t1); sticky(t1);
text(t2, (cx, 122), "2. Lebesgue - slice the range"); size(t2, 30); color(t2, gold); hidden(t2); sticky(t2);
text(t3, (cx, 122), "3. Henstock-Kurzweil - slice with a gauge"); size(t3, 30); color(t3, magenta); hidden(t3); sticky(t3);
caption(in1, "Riemann - vertical strips of equal width", (cx, cy - 70), 32, cyan); hidden(in1);
caption(in2, "Lebesgue - horizontal layers weighed by measure", (cx, cy), 32, gold); hidden(in2);
caption(in3, "Henstock-Kurzweil - strip widths set by a gauge", (cx, cy + 70), 32, magenta); hidden(in3);
text(cap, (cx, h - 42), ""); size(cap, 26); sticky(cap);
// ---------- act wipes: glowing verticals that sweep the frame between acts ----------
line(wipe1, (-40, 140), (-40, h - 90)); stroke(wipe1, 4); color(wipe1, gold); glow(wipe1, 14);
line(wipe2, (-40, 140), (-40, h - 90)); stroke(wipe2, 4); color(wipe2, magenta); glow(wipe2, 14);
line(wipe3, (-40, 140), (-40, h - 90)); stroke(wipe3, 4); glow(wipe3, 14);
// ---------- axes ----------
arrow(xax, (gx - 30, gy), (gx + 4*sx + 50, gy)); untraced(xax); stroke(xax, 3);
arrow(yax, (gx, gy + 30), (gx, 175)); untraced(yax); stroke(yax, 3);
text(xl0, (gx, gy + 26), "0"); size(xl0, 22); color(xl0, dim); hidden(xl0);
text(xl4, (gx + 4*sx, gy + 26), "4"); size(xl4, 22); color(xl4, dim); hidden(xl4);
// ---------- the smooth bump (acts 1 & 2) ----------
plot(fplot, (gx, gy), sx, sy, "3*exp(-((x-2)^2))", (0, 4));
untraced(fplot); stroke(fplot, 4); gradient(fplot, blue, cyan, gold, 270);
dot(rider, (gx, gy), 7); color(rider, gold); glow(rider, 8); hidden(rider);
// Riemann: 8 coarse strips (declared 26px low, they RISE into place)
// + the SAMPLE DOTS f(x_i*) riding the curve top - the star of the definition
for i in 0..8 {
let xm = (i + 0.5)*0.5;
let hgt = 3*exp(-((xm - 2)^2));
rect(rc{i}, (gx + xm*sx, gy - hgt*sy/2 + 26), 0.5*sx - 3, hgt*sy);
color(rc{i}, cyan); opacity(rc{i}, 0.55); tag(rc{i}, coarse);
dot(sm{i}, (gx + xm*sx, gy - hgt*sy), 5);
color(sm{i}, cyan); glow(sm{i}, 6); tag(sm{i}, samples);
}
hidden(coarse); hidden(samples);
// Riemann: 32 fine strips
for i in 0..32 {
let xm = (i + 0.5)*0.125;
let hgt = 3*exp(-((xm - 2)^2));
rect(rf{i}, (gx + xm*sx, gy - hgt*sy/2), 0.125*sx - 2, hgt*sy);
color(rf{i}, cyan); opacity(rf{i}, 0.7); tag(rf{i}, fine);
}
hidden(fine);
// the limit: swept exact area + live integral readout
area(ar, fplot, 0, 0); color(ar, cyan); opacity(ar, 0.35);
integral(intg, fplot, 0, 0, (gx + 4*sx - 40, 265)); hidden(intg);
// the pivot strip: one vertical slice that ROTATES into a horizontal one
rect(pv, (gx + 2*sx, gy - 1.5*sy), 42, 3*sy); color(pv, cyan); opacity(pv, 0.7); hidden(pv);
// Lebesgue: 10 horizontal layers (declared offset sideways, they SLIDE in alternating)
for j in 0..10 {
let ym = (j + 0.5)*0.3;
let hf = sqrt(ln(3/ym));
let jm = j - 2*floor(j/2);
let xoff = 180*jm - 90;
rect(ls{j}, (gx + 2*sx - xoff, gy - ym*sy), 2*hf*sx, 0.3*sy - 2);
color(ls{j}, gold); opacity(ls{j}, 0.55); tag(ls{j}, layers);
}
hidden(layers);
// the level scanner: a dashed line that sweeps UP through the layers
line(lev, (gx, gy - 0.15*sy), (gx + 4*sx, gy - 0.15*sy));
dashed(lev); color(lev, gold); stroke(lev, 2); hidden(lev);
// ---------- the spiky function (act 3) ----------
plot(gplot, (gx, gy), sx, sy, "1.1+1.7*exp(-40*((x-3)^2))", (0, 4));
untraced(gplot); stroke(gplot, 4); gradient(gplot, teal, magenta, 270);
dot(rider2, (gx, gy), 7); color(rider2, magenta); glow(rider2, 8); hidden(rider2);
// the gauge ITSELF, drawn as a real function: delta(x) dips where f spikes
plot(dplot, (gx, gy), sx, 60, "0.55-0.45*exp(-20*((x-3)^2))", (0, 4));
untraced(dplot); dashed(dplot); stroke(dplot, 2); color(dplot, magenta); opacity(dplot, 0.7);
text(dlab, (gx + 4*sx + 34, gy - 33), `$\delta(x)$`); size(dlab, 24); color(dlab, magenta); hidden(dlab);
// uniform cells first (they rise in) - two of them will FAIL at the spike
for i in 0..8 {
let xm = (i + 0.5)*0.5;
let hgt = 1.1 + 1.7*exp(-40*((xm - 3)^2));
rect(hku{i}, (gx + xm*sx, gy - hgt*sy/2 + 26), 0.5*sx - 3, hgt*sy);
color(hku{i}, teal); opacity(hku{i}, 0.5); tag(hku{i}, hk);
}
hidden(hk);
// gauge-fine cells + their TAG POINTS t_i pinned on the curve
for i in 0..10 {
let xm = 2.5 + (i + 0.5)*0.1;
let hgt = 1.1 + 1.7*exp(-40*((xm - 3)^2));
rect(nk{i}, (gx + xm*sx, gy - hgt*sy/2), 0.1*sx - 2, hgt*sy);
color(nk{i}, magenta); opacity(nk{i}, 0.75); tag(nk{i}, gauge); tag(nk{i}, hk);
dot(tg{i}, (gx + xm*sx, gy - hgt*sy), 3);
color(tg{i}, magenta); glow(tg{i}, 5); tag(tg{i}, tgd);
}
hidden(gauge); hidden(tgd);
bracelabel(bnar, (gx + 2.5*sx, gy + 16), (gx + 3.5*sx, gy + 16), `gauge $\delta(x)$ shrinks here`, 14);
color(bnar, magenta); hidden(bnar);
// ---------- equations (each one LIVES: it rewrites, then turns to dust) ----------
equation(eqR, (cx, 190), `S_{8} \;=\; \sum_{i=1}^{8} f(x_i^{*})\,\textcolor{cyan}{\Delta x}`, 40); hidden(eqR); sticky(eqR);
equation(eqL, (cx, 190), `\text{layer}_j \;=\; \textcolor{gold}{\mu\{\,f > t_j\,\}}\cdot\Delta t`, 40); hidden(eqL); sticky(eqL);
equation(eqH, (cx, 190), `\sum_i f(\textcolor{magenta}{t_i})\,\Delta x_i`, 38); hidden(eqH); sticky(eqH);
equation(eqS, (cx, 250), `\textcolor{cyan}{\text{Riemann}} \;\subsetneq\; \textcolor{gold}{\text{Lebesgue}} \;\subsetneq\; \textcolor{magenta}{\text{Henstock-Kurzweil}}`, 44); hidden(eqS); sticky(eqS); z(eqS, 10);
// ---------- finale confetti + triptych reprise of the three motifs ----------
circle(confbox, (cx, 290), 90); hidden(confbox);
particles(conf, confbox, 24, 4, 11, "ring"); color(conf, gold); hidden(conf);
let mxr = cx - 380;
let mxh = cx + 380;
let myb = 490;
// mini Riemann: a tiny staircase of vertical strips
for i in 0..6 {
let xm = (i + 0.5)/3;
let hgt = 76*exp(-2*((xm - 1)^2));
rect(mr{i}, (mxr - 110 + xm*110, myb - hgt/2), 34, hgt);
color(mr{i}, cyan); opacity(mr{i}, 0.6); tag(mr{i}, minir);
}
// mini Lebesgue: a tiny layer cake
for j in 0..5 {
let wd = 200 - 34*j;
rect(ml{j}, (cx, myb - 9 - j*17), wd, 14);
color(ml{j}, gold); opacity(ml{j}, 0.6); tag(ml{j}, minil);
}
// mini Henstock-Kurzweil: coarse cells, then a burst of fine ones at the spike
for i in 0..3 {
rect(mw{i}, (mxh - 90 + i*42, myb - 19), 40, 38);
color(mw{i}, teal); opacity(mw{i}, 0.6); tag(mw{i}, minih);
}
for k in 0..5 {
let hgt = 38 + 52*exp(-0.7*((k - 2)^2));
rect(mn{k}, (mxh + 10 + (k + 0.5)*11, myb - hgt/2), 9, hgt);
color(mn{k}, magenta); opacity(mn{k}, 0.75); tag(mn{k}, minih);
}
rect(mt, (mxh + 88, myb - 19), 40, 38); color(mt, teal); opacity(mt, 0.6); tag(mt, minih);
hidden(minir); hidden(minil); hidden(minih);
caption(ms1, "slice the domain", (mxr, 560), 26, cyan); hidden(ms1);
caption(ms2, "slice the range", (cx, 560), 26, gold); hidden(ms2);
caption(ms3, "slice by a gauge", (mxh, 560), 26, magenta); hidden(ms3);
// ================= timeline =================
// ---- cold open: title types on, the three names POP word by word
type(head, 1.2);
stagger(0.5) { wordpop(in1, 0.05); wordpop(in2, 0.05); wordpop(in3, 0.05); }
wait(1.2);
par { fade(in1, 0.4); fade(in2, 0.4); fade(in3, 0.4); }
// ---- act 1: RIEMANN ----------------------------------------------------
cue(whoosh);
show(t1, 0.5);
par { draw(xax, 0.6); draw(yax, 0.6); show(xl0, 0.6); show(xl4, 0.6); }
show(rider, 0.2);
par { draw(fplot, 1.5); travel(rider, fplot, 1.5, smooth); }
fade(rider, 0.3);
say(cap, "Slice the domain into equal strips; each SAMPLE DOT sets its strip's height", 0.5);
stagger(0.08) { for i in 0..8 { par { show(rc{i}, 0.3); shift(rc{i}, (0, -26), 0.4, out); show(sm{i}, 0.3); } } }
show(eqR, 0.6);
wait(1.2);
// push IN while the strips refine, equation rewrites underneath
say(cap, "Refine: quadruple the strips - the staircase hugs the curve", 0.5);
par { cam((cx, gy - 1.2*sy), 1.1, smooth); zoom(1.45, 1.1, smooth); }
par { fade(coarse, 0.5); fade(samples, 0.5); stagger(0.02) { for i in 0..32 { show(rf{i}, 0.22); } } }
cue(tick);
rewrite(eqR, `S_{32} \;=\; \sum_{i=1}^{32} f(x_i^{*})\,\textcolor{cyan}{\Delta x}`, 0.8);
wait(0.9);
par { cam((cx, cy), 1.0, smooth); zoom(1, 1.0, smooth); }
// the limit: strips dissolve into the EXACT swept area, the number climbs
cue(tick);
rewrite(eqR, `\int_0^{4} f\,dx \;=\; \lim_{n\to\infty} S_n`, 0.9);
say(cap, "In the limit, the sum becomes the exact area", 0.5);
show(intg, 0.3);
par { fade(fine, 1.2); to(ar, x, 4, 2.2, smooth); to(intg, x, 4, 2.2, smooth); }
flash(intg, gold);
wait(1.0);
par { shift(wipe1, (w + 80, 0), 0.7, linear); fade(ar, 0.5); fade(intg, 0.5); disintegrate(eqR, 0.8); fade(t1, 0.5); }
// ---- act 2: LEBESGUE ---------------------------------------------------
cue(whoosh);
show(t2, 0.5);
say(cap, "Turn the slicing sideways: cut the range, not the domain", 0.5);
show(pv, 0.3);
par { rotate(pv, 90, 0.9, smooth); recolor(pv, gold, 0.9); }
fade(pv, 0.4);
show(eqL, 0.6);
show(lev, 0.3);
par {
stagger(0.1) { for j in 0..10 { par { show(ls{j}, 0.35); shift(ls{j}, (180*(j - 2*floor(j/2)) - 90, 0), 0.5, out); } } }
shift(lev, (0, -2.7*sy), 1.9, smooth);
zoom(1.06, 2.2, smooth);
}
// the stack breathes: a travelling wave rolls up through the layers
par { for j in 0..10 { oscillate(ls{j}, y, 2.4, 3, j/10, 3.2); } pulse(ls4, 0.9); }
say(cap, "A layer at height t sits over the set where f > t; its width is that set's measure", 0.5);
cue(tick);
rewrite(eqL, `\int f\,d\mu \;=\; \int_0^{\infty}\textcolor{gold}{\mu\{\,f > t\,\}}\,dt`, 0.9);
wait(1.2);
fade(lev, 0.4);
par { shift(wipe2, (w + 80, 0), 0.7, linear); fade(layers, 0.5); disintegrate(eqL, 0.8); fade(t2, 0.5); fade(fplot, 0.5); zoom(1, 0.8, smooth); }
// ---- act 3: HENSTOCK-KURZWEIL ------------------------------------------
cue(whoosh);
show(t3, 0.5);
show(rider2, 0.2);
par { draw(gplot, 1.4); travel(rider2, gplot, 1.4, smooth); }
fade(rider2, 0.3);
say(cap, "New function, old recipe: equal strips again...", 0.5);
stagger(0.07) { for i in 0..8 { par { show(hku{i}, 0.3); shift(hku{i}, (0, -26), 0.4, out); } } }
show(eqH, 0.6);
wait(0.5);
// push in on the spike: the uniform cells FAIL, flash red, shake, shatter
par { cam((gx + 3*sx, gy - 1.3*sy), 1.1, smooth); zoom(1.6, 1.1, smooth); }
say(cap, "The spike slips between samples - equal widths are too coarse here", 0.5);
par { recolor(hku5, red, 0.3); recolor(hku6, red, 0.3); shake(hku5, 0.8); shake(hku6, 0.8); }
cue(pop);
par { disintegrate(hku5, 0.8); disintegrate(hku6, 0.8); }
// the gauge appears AS A FUNCTION: it dips exactly where f is dangerous
say(cap, `The gauge $\delta(x)$ is itself a function - watch it dip where f spikes`, 0.5);
par { draw(dplot, 1.0); show(dlab, 0.6); }
say(cap, `Tiny slices where f is wild - and every slice carries its tag point $t_i$`, 0.5);
stagger(0.05) { for i in 0..10 { par { show(nk{i}, 0.22); show(tg{i}, 0.22); } } }
show(bnar, 0.5);
wait(0.8);
par { cam((cx, cy), 1.0, smooth); zoom(1, 1.0, smooth); }
cue(tick);
rewrite(eqH, `\Delta x_i < \textcolor{magenta}{\delta(t_i)} \;\Longrightarrow\; \Big|\sum_i f(t_i)\,\Delta x_i - A\Big| < \varepsilon`, 0.9);
flow(gplot, 1.5, forward, once);
wait(1.2);
// ---- finale: wipe to black, hierarchy + confetti + triptych reprise ------
par {
shift(wipe3, (w + 80, 0), 0.7, linear);
fade(hk, 0.5); fade(tgd, 0.5); fade(gplot, 0.5); fade(dplot, 0.5); fade(dlab, 0.5);
fade(xax, 0.5); fade(yax, 0.5); fade(xl0, 0.5); fade(xl4, 0.5);
disintegrate(eqH, 0.8); fade(bnar, 0.5); fade(t3, 0.5);
}
cue(chime);
show(eqS, 0.8);
par {
breathe(eqS, 3, 0.05, 0, 7);
seq { show(conf, 0.1); burst(conf, 1.4); }
seq {
stagger(0.35) {
stagger(0.05) { for i in 0..6 { show(mr{i}, 0.25); } }
stagger(0.06) { for j in 0..5 { show(ml{j}, 0.25); } }
seq { stagger(0.04) { for i in 0..3 { show(mw{i}, 0.2); } } stagger(0.04) { for k in 0..5 { show(mn{k}, 0.2); } } show(mt, 0.2); }
}
stagger(0.3) { wordpop(ms1, 0.06); wordpop(ms2, 0.06); wordpop(ms3, 0.06); }
}
}
say(cap, "One area - three philosophies of slicing", 0.6);
wait(2.5);
disc-integration
A flat 2D region spins up into a 3D solid of revolution: one living equation rewrites from the profile r=√z to the volume ∫πr² dz, a single strip lifts into a disc, and a live volume readout climbs.
title("Disc Integration");
canvas(1000, 1000);
template("black");
// ================= HUD (2D always draws above the 3D world) =================
text(head, (cx, 60), "Disc Integration"); display(head); cursor(head);
text(t1, (cx, 122), "1. A region under a curve"); size(t1, 30); color(t1, gold); hidden(t1);
text(t2, (cx, 122), "2. Spin it - every strip sweeps a disc"); size(t2, 30); color(t2, cyan); hidden(t2);
text(t3, (cx, 122), "3. Stack the discs - integrate"); size(t3, 30); color(t3, magenta); hidden(t3);
text(cap, (cx, h - 42), ""); size(cap, 26);
// one living equation: it rewrites its way from the curve to the volume
equation(eqM, (cx, 190), `y=\sqrt{x}\quad(0\le x\le 4)`, 40); hidden(eqM);
// live volume readout
counter(vol, (w - 185, 300), 0, 2, "V = ", ""); hidden(vol);
// ================= act 1 cast: the flat 2D region =================
let gx = cx - 300;
let gy = 520;
let s2 = 140;
arrow(xax2, (gx - 30, gy), (gx + 4*s2 + 50, gy)); untraced(xax2); stroke(xax2, 3); tag(xax2, d2);
arrow(yax2, (gx, gy + 30), (gx, gy - 2.4*s2)); untraced(yax2); stroke(yax2, 3); tag(yax2, d2);
text(x0b, (gx, gy + 26), "0"); size(x0b, 22); color(x0b, dim); hidden(x0b); tag(x0b, d2);
text(x4b, (gx + 4*s2, gy + 26), "4"); size(x4b, 22); color(x4b, dim); hidden(x4b); tag(x4b, d2);
plot(rplot, (gx, gy), s2, s2, "sqrt(x)", (0, 4));
untraced(rplot); stroke(rplot, 4); gradient(rplot, blue, cyan, gold, 270); tag(rplot, d2);
dot(rider, (gx, gy), 7); color(rider, gold); glow(rider, 8); hidden(rider); tag(rider, d2);
area(ar2, rplot, 0, 0); color(ar2, cyan); opacity(ar2, 0.3); tag(ar2, d2);
// the one strip we will follow into 3D (x = 2.25 - remember it)
rect(strip, (gx + 2.25*s2, gy - sqrt(2.25)*s2/2), 66, sqrt(2.25)*s2);
color(strip, cyan); opacity(strip, 0.75); hidden(strip); tag(strip, d2);
// ================= act 2-3 cast: the 3D world (right-handed, Z-up) ==========
camera3((8.2, -9.5, 5.0), (0, 0, 1.9), 42);
axes3(ax3, (0, 0, 0), 4.6, 1); hidden(ax3);
// the spin axis, called out explicitly
line3(spinax, (0, 0, -0.3), (0, 0, 4.6)); thick(spinax, 0.015); color(spinax, dim); hidden(spinax);
text(zlab, (cx, cy), "spin axis"); size(zlab, 22); color(zlab, dim); hidden(zlab);
pin3(zlab, (0, 0, 4.55), (0, -16));
// the profile curve, stood upright: r = sqrt(z)
curve3(prof, "sqrt(t)", "0", "t", (0, 4)); untraced(prof); thick(prof, 0.04); color(prof, gold);
// ghost of the swept surface: bright wireframe revolve
revolve3(ghost, (0, 0, 0), "sqrt(t)", (0, 4), 64);
finish3(ghost, "wire=1"); color(ghost, teal); opacity(ghost, 0.4); untraced(ghost);
// 8 coarse discs (thin cylinders = many-sided prisms), declared low so they RISE
for i in 0..8 {
let zc = (i + 0.5)*0.5;
prism3(dc{i}, (0, 0, zc - 0.45), 48, sqrt(zc), 0.48);
color(dc{i}, cyan); opacity(dc{i}, 0.85); tag(dc{i}, coarse3);
}
hidden(coarse3);
// 20 fine discs
for i in 0..20 {
let zc = (i + 0.5)*0.2;
prism3(df{i}, (0, 0, zc - 0.3), 48, sqrt(zc), 0.19);
color(df{i}, cyan); opacity(df{i}, 0.9); tag(df{i}, fine3);
}
hidden(fine3);
// radius callout for the pulled-out disc (z = 2.25, r = 1.5, pulled to x + 3)
arrow3(rarr, (3.0, 0, 2.62), (4.5, 0, 2.62)); thick(rarr, 0.03); color(rarr, gold); untraced(rarr);
text(rlab, (cx, cy), `$r = \sqrt{z}$`); size(rlab, 26); color(rlab, gold); hidden(rlab);
pin3(rlab, (3.75, 0, 2.62), (0, -30));
// the exact solid, and the vase it will morph into ("any profile, same recipe")
revolve3(solid, (0, 0, 0), "sqrt(t)", (0, 4), 64);
finish3(solid, "shading=smooth depth=0.2 shadow=0.2"); color(solid, cyan); hidden(solid);
revolve3(vase, (0, 0, 0), "1.1+0.55*sin(1.7*t-0.9)", (0, 4), 64); hidden(vase);
morph3(solid, vase);
// ================= timeline =================
// ---- cold open
type(head, 1.1);
// ---- act 1: the flat region
cue(whoosh);
show(t1, 0.5);
par { draw(xax2, 0.6); draw(yax2, 0.6); show(x0b, 0.6); show(x4b, 0.6); }
show(rider, 0.2);
par { draw(rplot, 1.4); travel(rider, rplot, 1.4, smooth); }
fade(rider, 0.3);
show(eqM, 0.6);
say(cap, "Take the region under the curve...", 0.5);
to(ar2, x, 4, 1.6, smooth);
say(cap, "...and keep your eye on this one strip", 0.5);
show(strip, 0.4);
pulse(strip, 0.9);
wait(0.8);
// ---- the pivot: leave flatland
say(cap, "Now stand the region upright - and SPIN it around the axis", 0.5);
cue(whoosh);
fade(d2, 0.8);
show(t2, 0.5); fade(t1, 0.3);
par { show(ax3, 0.8); show(spinax, 0.8); show(zlab, 0.8); }
draw(prof, 1.2);
flash(spinax, magenta);
// THE MONEY SHOT: the profile curve physically sweeps 360 degrees
// around the axis while the wireframe surface traces on beneath it
par {
turn3(prof, (0, 0, 2), z, 360, 2.6, smooth);
draw(ghost, 2.6);
orbit3(-20, 18, 12.0, 2.6, smooth);
}
cue(tick);
rewrite(eqM, `dV \;=\; \pi\,\textcolor{cyan}{r^2}\,dz`, 0.9);
say(cap, "Every strip of area sweeps a thin DISC of volume", 0.5);
// ---- act 3: the disc stack rises
show(t3, 0.5); fade(t2, 0.3);
stagger(0.09) { for i in 0..8 { par { show(dc{i}, 0.3); shift3(dc{i}, (0, 0, 0.45), 0.4, out); } } }
wait(0.4);
// pull ONE disc out of the stack - the same strip from act 1
say(cap, "That same strip from before - now a cylinder: radius sqrt(z), thickness dz", 0.5);
cue(pop);
shift3(dc4, (3.0, 0, 0), 0.9, smooth);
par { draw(rarr, 0.5); show(rlab, 0.5); }
wait(1.2);
par { fade(rarr, 0.4); fade(rlab, 0.4); shift3(dc4, (-3.0, 0, 0), 0.9, smooth); }
cue(tick);
rewrite(eqM, `V \;\approx\; \sum_i \pi\,r(z_i)^2\,\Delta z`, 0.9);
wait(0.8);
// refine: thinner discs, camera drifts around the stack
say(cap, "Slice thinner - the stack hugs the surface", 0.5);
par {
fade(coarse3, 0.6);
stagger(0.04) { for i in 0..20 { show(df{i}, 0.22); } }
orbit3(35, 24, 11.0, 2.2, smooth);
}
cue(tick);
rewrite(eqM, `V \;=\; \pi\!\int_0^{4}(\sqrt{z}\,)^2\,dz \;=\; \pi\!\int_0^{4} z\,dz`, 0.9);
wait(0.8);
// the limit: discs dissolve into the exact smooth solid, the number climbs
say(cap, "In the limit: the exact solid of revolution", 0.5);
show(vol, 0.3);
par { fade(fine3, 1.2); fade(ghost, 1.2); show(solid, 1.4); to(vol, value, 25.13, 2.2); }
flash(vol, gold);
cue(tick);
rewrite(eqM, `V \;=\; \pi\cdot\tfrac{16}{2} \;=\; 8\pi \;\approx\; 25.13`, 0.9);
par { orbit3(90, 20, 10.5, 2.6, smooth); }
wait(0.6);
// ---- flourish: ANY profile, same recipe - the solid becomes a vase
say(cap, "Any profile, same recipe - just change r(z)", 0.5);
cue(whoosh);
par {
to(solid, morph, 1, 2.6, smooth);
fade(vol, 0.8);
rewrite(eqM, `V \;=\; \pi\int_a^b r(z)^2\,dz`, 1.2);
orbit3(160, 16, 10.5, 2.6, smooth);
}
cue(chime);
par {
breathe(eqM, 3, 0.05, 0, 6);
orbit3(200, 14, 11.0, 6, smooth);
}
say(cap, "Area swept in a circle - volume, one disc at a time", 0.6);
wait(2.0);
reactive-integral
The reactive-math gold example: integration by parts stays one continuous equation,
then a plot verifies that the antiderivative’s slope is the integrand and +C is a
family of vertical translations. Uses rewrite, not hand-positioned formula pieces.
// ============================================================================
// reactive-integral.manic — one calculation, one continuous visual world
// ----------------------------------------------------------------------------
// Integration by parts uses one structured equation. `rewrite` keeps unchanged
// RaTeX parts alive while only the mathematical difference moves or enters.
// The graph then verifies the result: the slope of F is the height of f, and
// +C becomes a visible family of vertical translations.
// ============================================================================
title("A Reactive Integral");
canvas("16:9");
template("plain");
watermark(manicMark, (1135, 30), "Made With Manic");
// Shared colour roles follow the same mathematical objects through every act.
// x / u is magenta; cos, sin and their antiderivative relation are cyan.
text(kicker, (285, 38), "ONE INTEGRAL · ONE CONTINUOUS STORY");
size(kicker, 18); color(kicker, dim); bold(kicker); hidden(kicker);
text(headline, (430, 72), "Watch only the mathematics that changes");
size(headline, 28); color(headline, fg); bold(headline); hidden(headline);
// ---------------------------------------------------------------------------
// ONE PERSISTENT EQUATION
// ---------------------------------------------------------------------------
equation(
work,
(cx, 145),
`\int \textcolor{magenta}{x}\,\textcolor{cyan}{\cos(x)}\,dx`,
46
);
hidden(work);
// The by-parts roles remain visible while the expression changes above them.
equation(uChoice, (330, 225), `u=x`, 28); color(uChoice, magenta); hidden(uChoice);
equation(dvChoice, (525, 225), `dv=\cos(x)\,dx`, 28); color(dvChoice, cyan); hidden(dvChoice);
equation(duChoice, (760, 225), `du=dx`, 28); color(duChoice, magenta); hidden(duChoice);
equation(vChoice, (950, 225), `v=\sin(x)`, 28); color(vChoice, cyan); hidden(vChoice);
text(method, (cx, 278), "INTEGRATION BY PARTS");
size(method, 16); color(method, dim); bold(method); hidden(method);
// ---------------------------------------------------------------------------
// ONE GRAPH, ASKED THREE CONNECTED QUESTIONS
// ---------------------------------------------------------------------------
let ox = cx;
let oy = 515;
let sx = 108;
let sy = 28;
axes(ax, (ox, oy), 550, 165, 1);
color(ax, dim); opacity(ax, 0.50); untraced(ax);
// Integrand and one representative antiderivative (C=0).
plot(f, (ox, oy), sx, sy, "x*cos(x)", (-5, 5));
color(f, cyan); stroke(f, 5); glow(f, 0.8); untraced(f);
plot(bigF, (ox, oy), sx, sy, "x*sin(x)+cos(x)", (-5, 5));
color(bigF, lime); stroke(bigF, 5); dashed(bigF, 18, 11);
glow(bigF, 0.8); untraced(bigF);
// Numerical derivative of F: drawing it lands directly on f.
deriv(dF, bigF);
color(dF, gold); stroke(dF, 3); glow(dF, 0.5); untraced(dF);
// A moving tangent makes “F' = f” a local statement at every x.
tangent(tan, bigF, -3.8, 180); color(tan, gold); hidden(tan);
slope(slopeValue, bigF, -3.8, (18, -24)); color(slopeValue, gold); hidden(slopeValue);
equation(fLabel, (250, 338), `f(x)=x\cos(x)`, 28); color(fLabel, cyan); hidden(fLabel);
equation(FLabel, (1010, 338), `F(x)=x\sin(x)+\cos(x)`, 28); color(FLabel, lime); hidden(FLabel);
equation(proofLabel, (cx, 280), `F'(x)=f(x)=x\cos(x)`, 34); color(proofLabel, gold); hidden(proofLabel);
// +C does not change the derivative: it translates the same curve vertically.
plot(Fup, (ox, oy), sx, sy, "x*sin(x)+cos(x)+1.5", (-5, 5));
plot(Fdown, (ox, oy), sx, sy, "x*sin(x)+cos(x)-1.5", (-5, 5));
color(Fup, magenta); color(Fdown, magenta);
stroke(Fup, 3); stroke(Fdown, 3); opacity(Fup, 0.55); opacity(Fdown, 0.55);
dashed(Fup, 14, 9); dashed(Fdown, 14, 9);
untraced(Fup); untraced(Fdown);
equation(familyLabel, (cx, 280), `F(x)+C\quad\text{changes height, not slope}`, 32);
color(familyLabel, magenta); hidden(familyLabel);
text(caption, (cx, 690), "");
size(caption, 19); color(caption, dim); bold(caption);
// Axes and curves are `untraced`, so their draw progress starts at zero without
// also zeroing opacity. That lets `draw(...)` reveal their geometry later.
// ---------------------------------------------------------------------------
// TIMELINE
// ---------------------------------------------------------------------------
show(kicker, 0.45);
show(headline, 0.55);
say(caption, "Start with the question. Keep the stage — change only its state.");
show(work, 0.65);
wait(1.60);
// Choose u and dv by reusing the same semantic colours.
show(method, 0.35);
say(caption, "Choose the two roles. Colour preserves their identity.");
par { show(uChoice, 0.50); show(dvChoice, 0.65); }
wait(1.20);
par { show(duChoice, 0.50); show(vChoice, 0.50); }
wait(1.50);
// Build x sin(x) - integral sin(x) dx. Reusable glyphs travel to their new
// jobs while the obsolete cosine is the only original factor that leaves.
say(caption, "Apply integration by parts: persistent pieces move; new pieces arrive.");
rewrite(
work,
`\textcolor{magenta}{x}\textcolor{cyan}{\sin(x)}-\int\textcolor{cyan}{\sin(x)}\,dx`,
1.05,
smooth
);
wait(2.00);
// Only the remaining integral changes. x sin(x) stays untouched.
say(caption, "Resolve the remaining integral. The stable product never blinks.");
rewrite(
work,
`\textcolor{magenta}{x}\textcolor{cyan}{\sin(x)}+\textcolor{cyan}{\cos(x)}+\textcolor{gold}{C}`,
1.00,
smooth
);
wait(2.00);
// Move attention from symbolic manipulation into the same idea as geometry.
say(caption, "Now verify the answer visually — without leaving the scene.");
par {
fade(method, 0.40);
fade(uChoice, 0.40); fade(dvChoice, 0.40);
fade(duChoice, 0.40); fade(vChoice, 0.40);
draw(ax, 0.85);
}
draw(f, 1.80);
show(fLabel, 0.45);
wait(1.00);
draw(bigF, 1.80);
show(FLabel, 0.45);
wait(1.00);
// The tangent and its numeric slope glide continuously across F.
say(caption, "At every x, the slope of the green curve equals the blue height.");
par { show(tan, 0.45); show(slopeValue, 0.45); }
par {
to(tan, x, 3.8, 4.80, smooth);
to(slopeValue, x, 3.8, 4.80, smooth);
}
wait(0.60);
// The derivative trace lands directly over the original integrand.
show(proofLabel, 0.50);
draw(dF, 2.20);
flash(f, cyan);
wait(1.40);
// Finish with the geometric meaning of the integration constant.
say(caption, "+C creates a family of answers — same shape, same slope.");
par {
fade(tan, 0.45); fade(slopeValue, 0.45); fade(dF, 0.45);
fade(proofLabel, 0.45); fade(fLabel, 0.45); fade(FLabel, 0.45);
show(familyLabel, 0.55);
pulse(work, 0.70);
}
par { draw(Fup, 1.50); draw(Fdown, 1.50); }
wait(2.80);
rewrite-integration
A full manual-integration walkthrough, animated: one equation object is rewrite-morphed
through all 21 steps — every substitution, partial-fraction split and back-substitution,
each captioned with its rule — down to the closed form. Display-quality LaTeX throughout.
title("Rewrite Integration Steps");
canvas("16:9");
template("mono");
text(head, (cx, 58), "Integrating by rewriting the denominator");
display(head);
bold(head);
size(head, 32);
hidden(head);
text(sub, (cx, 94), "Manual integration — partial fractions by substitution");
size(sub, 20);
color(sub, dim);
hidden(sub);
rect(panelBox, (cx, cy + 14), w - 150, 380);
outline(panelBox, dim);
opacity(panelBox, 0.35);
hidden(panelBox);
text(stepTitle, (cx, 150), "");
display(stepTitle);
bold(stepTitle);
size(stepTitle, 26);
color(stepTitle, cyan);
text(ruleTag, (cx, 182), "");
size(ruleTag, 18);
color(ruleTag, dim);
text(watermark, (w - 96, h - 12), "Made with Manic");
size(watermark, 15);
color(watermark, dim);
hidden(watermark);
equation(work, (cx, cy + 20), `\int \frac{1}{(x - 1)(x + 2)}\,dx`, 34);
hidden(work);
equation(finalLine, (cx, h - 64), `\boxed{\,\frac{\log{\left(x - 1 \right)}}{3} - \frac{\log{\left(x + 2 \right)}}{3} + C\,}`, 34);
color(finalLine, lime);
hidden(finalLine);
show(head, 0.45);
show(sub, 0.45);
show(panelBox, 0.45);
show(watermark, 0.45);
show(work, 0.5);
wait(0.4);
par {
say(stepTitle, "1. u-Substitution", 0.25);
say(ruleTag, "Rule: URule", 0.25);
rewrite(work, `Let\ u = x - 1,\quad \frac{du}{dx} = 1`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "2. u-Substitution", 0.25);
say(ruleTag, "Rule: URule", 0.25);
rewrite(work, `Let\ u = u + \frac{3}{2},\quad \frac{du}{d_u} = 1`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "3. Rewrite", 0.25);
say(ruleTag, "Rule: RewriteRule", 0.25);
rewrite(work, `\int \frac{1}{3 u + \left(u - \frac{3}{2}\right)^{2} - \frac{9}{2}} \,d_u = \int - \frac{2}{3 \left(2 u + 3\right)} + \frac{2}{3 \left(2 u - 3\right)} \,d_u`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "4. Sum rule", 0.25);
say(ruleTag, "Rule: AddRule", 0.25);
rewrite(work, `\int (f+g)\,d_u = \int f\,d_u + \int g\,d_u`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "5. Constant factor", 0.25);
say(ruleTag, "Rule: ConstantTimesRule", 0.25);
rewrite(work, `\int - \frac{2}{3 \left(2 u + 3\right)} \,d_u = - \frac{2}{3} \int \frac{1}{2 u + 3} \,d_u`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "6. u-Substitution", 0.25);
say(ruleTag, "Rule: URule", 0.25);
rewrite(work, `Let\ u = 2 u + 3,\quad \frac{du}{d_u} = 2`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "7. Constant factor", 0.25);
say(ruleTag, "Rule: ConstantTimesRule", 0.25);
rewrite(work, `\int \frac{1}{2 u} \,d_u = \frac{1}{2} \int \frac{1}{u} \,d_u`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "8. Reciprocal", 0.25);
say(ruleTag, "Rule: ReciprocalRule", 0.25);
rewrite(work, `\int \frac{1}{u} \,d_u = \ln|u| + C = \log{\left(u \right)}`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "9. Simplify", 0.25);
say(ruleTag, "Rule: ConstantTimesRule", 0.25);
rewrite(work, `= \frac{\log{\left(u \right)}}{2} + C`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "10. Back substitute", 0.25);
say(ruleTag, "Rule: URule", 0.25);
rewrite(work, `= \frac{\log{\left(2 u + 3 \right)}}{2} + C`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "11. Simplify", 0.25);
say(ruleTag, "Rule: ConstantTimesRule", 0.25);
rewrite(work, `= - \frac{\log{\left(2 u + 3 \right)}}{3} + C`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "12. Constant factor", 0.25);
say(ruleTag, "Rule: ConstantTimesRule", 0.25);
rewrite(work, `\int \frac{2}{3 \left(2 u - 3\right)} \,d_u = \frac{2}{3} \int \frac{1}{2 u - 3} \,d_u`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "13. u-Substitution", 0.25);
say(ruleTag, "Rule: URule", 0.25);
rewrite(work, `Let\ u = 2 u - 3,\quad \frac{du}{d_u} = 2`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "14. Constant factor", 0.25);
say(ruleTag, "Rule: ConstantTimesRule", 0.25);
rewrite(work, `\int \frac{1}{2 u} \,d_u = \frac{1}{2} \int \frac{1}{u} \,d_u`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "15. Reciprocal", 0.25);
say(ruleTag, "Rule: ReciprocalRule", 0.25);
rewrite(work, `\int \frac{1}{u} \,d_u = \ln|u| + C = \log{\left(u \right)}`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "16. Simplify", 0.25);
say(ruleTag, "Rule: ConstantTimesRule", 0.25);
rewrite(work, `= \frac{\log{\left(u \right)}}{2} + C`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "17. Back substitute", 0.25);
say(ruleTag, "Rule: URule", 0.25);
rewrite(work, `= \frac{\log{\left(2 u - 3 \right)}}{2} + C`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "18. Simplify", 0.25);
say(ruleTag, "Rule: ConstantTimesRule", 0.25);
rewrite(work, `= \frac{\log{\left(2 u - 3 \right)}}{3} + C`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "19. Combine", 0.25);
say(ruleTag, "Rule: AddRule", 0.25);
rewrite(work, `= \frac{\log{\left(2 u - 3 \right)}}{3} - \frac{\log{\left(2 u + 3 \right)}}{3} + C`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "20. Back substitute", 0.25);
say(ruleTag, "Rule: URule", 0.25);
rewrite(work, `= \frac{\log{\left(2 u \right)}}{3} - \frac{\log{\left(2 u + 6 \right)}}{3} + C`, 0.7, smooth);
}
wait(0.3);
par {
say(stepTitle, "21. Back substitute", 0.25);
say(ruleTag, "Rule: URule", 0.25);
rewrite(work, `= \frac{\log{\left(2 x - 2 \right)}}{3} - \frac{\log{\left(2 x + 4 \right)}}{3} + C`, 0.7, smooth);
}
wait(0.3);
show(finalLine, 0.8);
pulse(finalLine, 0.8);
wait(1.0);
reactive-world
The foundation for creator-reactive stories: five named step blocks keep one curve,
equation, tangent, live slope, derivative plot and caption synchronized. Each step is
also exported as a marker for seeking and future multi-format publishing.
// ============================================================================
// reactive-world.manic — one world, named state transitions
// ----------------------------------------------------------------------------
// Each `step` changes several existing representations together. The equation,
// tangent, live slope, derivative curve, caption and camera share one continuous
// stage; anything a step does not mention simply persists.
// ============================================================================
title("Reactive World — The Derivative Lives on the Curve");
canvas("9:16");
template("terminal");
watermark(manicMark, (865, 165), "Made With Manic");
creator(me, "@anish2good name=Calculus_World tagline=One_idea_many_views yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social cta=See_the_change safe=reels");
socials(me);
text(kicker, (540, 130), "ONE WORLD · FIVE NAMED STEPS");
size(kicker, 23); color(kicker, dim); bold(kicker); hidden(kicker);
text(headline, (540, 215), "The derivative lives on the curve");
size(headline, 39); color(headline, fg); bold(headline); hidden(headline);
rect(stage, (540, 855), 930, 1050);
color(stage, panel); outline(stage, dim); opacity(stage, 0.72);
equation(work, (540, 390), `f(x)=0.35x^2`, 51);
color(work, cyan); hidden(work);
let ox = 540;
let oy = 1050;
let sx = 115;
let sy = 62;
axes(ax, (ox, oy), 410, 420, 1);
color(ax, dim); opacity(ax, 0.55); untraced(ax);
plot(f, (ox, oy), sx, sy, "0.35*x*x", (-3.5, 3.5));
color(f, cyan); stroke(f, 6); glow(f, 0.8); untraced(f);
deriv(df, f);
color(df, magenta); stroke(df, 5); dashed(df, 18, 11); untraced(df);
tangent(tan, f, -2.8, 235);
color(tan, gold); stroke(tan, 4); hidden(tan);
slope(rate, f, -2.8, (24, -30));
color(rate, gold); hidden(rate);
dot(vertex, (ox, oy), 10);
color(vertex, lime); glow(vertex, 1.8); hidden(vertex);
equation(curveLabel, (790, 650), `f(x)`, 31);
color(curveLabel, cyan); hidden(curveLabel);
equation(derivLabel, (790, 1320), `f'(x)`, 31);
color(derivLabel, magenta); hidden(derivLabel);
text(caption, (540, 1450), "Start with one curve and one moving question: how steep is it?");
size(caption, 26); color(caption, dim); wrap(caption, 820); hidden(caption);
// ---------------------------------------------------------------------------
// NAMED REACTIVE STEPS
// ---------------------------------------------------------------------------
step("question") {
show(kicker, 0.40);
show(headline, 0.50);
show(work, 0.55);
draw(ax, 0.85);
draw(f, 1.40);
show(curveLabel, 0.45);
show(caption, 0.45);
}
wait(0.75);
step("measure-slope") {
rewrite(work, `f'(x)=0.70x`, 0.90, smooth);
show(tan, 0.45);
show(rate, 0.45);
to(tan, x, 2.8, 3.20, smooth);
to(rate, x, 2.8, 3.20, smooth);
say(caption, "Move the point: the tangent and its slope update together.", 0.40);
}
wait(0.60);
step("find-the-flat-point") {
rewrite(work, `f'(0)=\textcolor{lime}{0}`, 0.85, smooth);
to(tan, x, 0, 1.80, smooth);
to(rate, x, 0, 1.80, smooth);
show(vertex, 0.40);
say(caption, "At the vertex the tangent becomes flat, so the derivative is zero.", 0.40);
}
wait(0.65);
step("see-the-derivative") {
rewrite(work, `f'(x)=\textcolor{magenta}{0.70x}`, 0.90, smooth);
fade(tan, 0.40);
fade(rate, 0.40);
draw(df, 1.70);
show(derivLabel, 0.45);
say(caption, "Collect every local slope and a second curve appears: the derivative.", 0.40);
}
wait(0.70);
step("takeaway") {
rewrite(work, `\text{slope of }f=\textcolor{magenta}{f'}`, 0.95, smooth);
pulse(f, 0.70);
pulse(df, 0.70);
say(caption, "One idea, synchronized across symbols, geometry and motion.", 0.40);
}
wait(1.80);
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);
creator-trigonometry-one-angle
One reactive angle drives three synchronized unit circles, degree/radian readouts, projection measurements, graph markers and progressive sine/cosine/tangent traces. Tangent is split into truthful branches, so its marker disappears at π/2 instead of drawing a false line through the asymptote.
// One live angle drives three unit-circle constructions, graph traces,
// markers, projections, and numerical readouts. Tangent is deliberately split
// into branches so the animation never lies across an asymptote.
title("One Angle Creates Three Functions");
canvas("9:16");
template("black");
creator(me,"@anish2good name=Manic_Geometry tagline=Relationships_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Animate_the_relationship safe=clean");
socials(me);
watermark(mark,(w*0.18,h*0.065),"Made With Manic");
endcard(me,"title=Make_Math_Move cta=8gwifi.org/manic");
let u=(w+h-abs(w-h))/1080;
text(kicker,(cx,h*0.095),"MANIC · LIVE TRIGONOMETRY");
text(head,(cx,h*0.145),"One angle. Three different measurements.");
equation(identity,(cx,h*0.205),
`\textcolor{cyan}{y=\sin\alpha}\qquad
\textcolor{magenta}{y=\cos\alpha}\qquad
\textcolor{lime}{y=\tan\alpha}`,22*u);
counter(degrees,(w*0.33,h*0.255),20,0,"α = ","°");
counter(radians,(w*0.69,h*0.255),0.35,2,"α = "," rad");
text(caption,(cx,h*0.835),"Rotate one radius. Watch three graphs respond.");
size(kicker,19*u); bold(kicker); color(kicker,cyan);
size(head,29*u); bold(head); wrap(head,w*0.84);
size(degrees,20*u); bold(degrees); color(degrees,gold);
size(radians,20*u); bold(radians); color(radians,gold);
size(caption,20*u); bold(caption); color(caption,dim); wrap(caption,w*0.80);
// One authored angle is the only changing input in the entire construction.
// Two rotations are available: the first teaches, the second becomes a
// continuous performance pass. Circle formulas are naturally periodic; graph
// markers and public readouts wrap back into [0,2pi).
parameter(alpha,(cx,h*0.885),0.35,0.02,12.54,"ANGLE α",2);
color(alpha,cyan);
bind(alpha,degrees,value,"(p-6.283185*floor(p/6.283185))*180/pi");
bind(alpha,radians,value,"p-6.283185*floor(p/6.283185)");
// Shared portrait geometry.
let circleX=w*0.265;
let graphX=w*0.535;
let radius=w*0.092;
let graphSX=w*0.050;
let graphSY=radius;
let graphRight=graphX+graphSX*6.283;
let row1=h*0.385;
let row2=h*0.565;
let row3=h*0.745;
// ---------------------------------------------------------------------------
// SIN — the rotating point's height.
// ---------------------------------------------------------------------------
circle(sinCircle,(circleX,row1),radius);
outlined(sinCircle); outline(sinCircle,fg);
line(sinCX,(circleX-radius*1.22,row1),(circleX+radius*1.22,row1));
line(sinCY,(circleX,row1-radius*1.22),(circleX,row1+radius*1.22));
point(sinO,(circleX,row1));
point(sinP,(circleX+radius*cos(0.35),row1-radius*sin(0.35)));
point(sinHeight,(circleX,row1-radius*sin(0.35)));
segment(sinRay,sinO,sinP);
segment(sinGuide,sinP,sinHeight);
anglemark(sinAngle,sinHeight,sinO,sinP);
bind(alpha,sinP,x,"286.2+99.4*cos(p)");
bind(alpha,sinP,y,"739.2-99.4*sin(p)");
bind(alpha,sinHeight,y,"739.2-99.4*sin(p)");
line(sinGX,(graphX,row1),(graphRight,row1));
line(sinGY,(graphX,row1-graphSY*1.28),(graphX,row1+graphSY*1.28));
plot(sinCurve,(graphX,row1),graphSX,graphSY,"sin(x)",(0,6.283));
point(sinDot,(graphX+graphSX*0.35,row1-graphSY*sin(0.35)));
point(sinFoot,(graphX+graphSX*0.35,row1));
segment(sinDrop,sinDot,sinFoot);
bind(alpha,sinCurve,trace,"p/6.283185");
bind(alpha,sinDot,x,"577.8+54*(p-6.283185*floor(p/6.283185))");
bind(alpha,sinDot,y,"739.2-99.4*sin(p)");
bind(alpha,sinFoot,x,"577.8+54*(p-6.283185*floor(p/6.283185))");
text(sinLabel,(graphX+graphSX*2.2,row1-radius*1.45),"y = sin(α) · height");
color(sinCircle,fg); color(sinCX,dim); color(sinCY,dim);
color(sinRay,cyan); color(sinGuide,cyan); color(sinAngle,cyan);
color(sinP,cyan); color(sinHeight,cyan); color(sinO,fg);
color(sinGX,dim); color(sinGY,dim); color(sinCurve,cyan);
color(sinDot,fg); color(sinFoot,fg); color(sinDrop,cyan); color(sinLabel,cyan);
dashed(sinGuide,8,6); dashed(sinDrop,8,6);
stroke(sinCircle,3); stroke(sinRay,4); stroke(sinGuide,3); stroke(sinCurve,5);
size(sinLabel,18*u); bold(sinLabel);
// ---------------------------------------------------------------------------
// COS — the rotating point's horizontal reach.
// ---------------------------------------------------------------------------
circle(cosCircle,(circleX,row2),radius);
outlined(cosCircle); outline(cosCircle,fg);
line(cosCX,(circleX-radius*1.22,row2),(circleX+radius*1.22,row2));
line(cosCY,(circleX,row2-radius*1.22),(circleX,row2+radius*1.22));
point(cosO,(circleX,row2));
point(cosP,(circleX+radius*cos(0.35),row2-radius*sin(0.35)));
point(cosReach,(circleX+radius*cos(0.35),row2));
segment(cosRay,cosO,cosP);
segment(cosGuide,cosP,cosReach);
bind(alpha,cosP,x,"286.2+99.4*cos(p)");
bind(alpha,cosP,y,"1084.8-99.4*sin(p)");
bind(alpha,cosReach,x,"286.2+99.4*cos(p)");
line(cosGX,(graphX,row2),(graphRight,row2));
line(cosGY,(graphX,row2-graphSY*1.28),(graphX,row2+graphSY*1.28));
plot(cosCurve,(graphX,row2),graphSX,graphSY,"cos(x)",(0,6.283));
point(cosDot,(graphX+graphSX*0.35,row2-graphSY*cos(0.35)));
point(cosFoot,(graphX+graphSX*0.35,row2));
segment(cosDrop,cosDot,cosFoot);
bind(alpha,cosCurve,trace,"p/6.283185");
bind(alpha,cosDot,x,"577.8+54*(p-6.283185*floor(p/6.283185))");
bind(alpha,cosDot,y,"1084.8-99.4*cos(p)");
bind(alpha,cosFoot,x,"577.8+54*(p-6.283185*floor(p/6.283185))");
text(cosLabel,(graphX+graphSX*2.2,row2-radius*1.45),"y = cos(α) · reach");
color(cosCircle,fg); color(cosCX,dim); color(cosCY,dim);
color(cosRay,magenta); color(cosGuide,magenta);
color(cosP,magenta); color(cosReach,magenta); color(cosO,fg);
color(cosGX,dim); color(cosGY,dim); color(cosCurve,magenta);
color(cosDot,fg); color(cosFoot,fg); color(cosDrop,magenta); color(cosLabel,magenta);
dashed(cosGuide,8,6); dashed(cosDrop,8,6);
stroke(cosCircle,3); stroke(cosRay,4); stroke(cosGuide,3); stroke(cosCurve,5);
size(cosLabel,18*u); bold(cosLabel);
// ---------------------------------------------------------------------------
// TAN — where the radius meets the tangent x=1. Its graph uses three separate
// branches; no path is ever drawn through pi/2 or 3pi/2.
// ---------------------------------------------------------------------------
circle(tanCircle,(circleX,row3),radius);
outlined(tanCircle); outline(tanCircle,fg);
line(tanCX,(circleX-radius*1.22,row3),(circleX+radius*1.38,row3));
line(tanCY,(circleX,row3-radius*1.22),(circleX,row3+radius*1.22));
line(tangentWall,(circleX+radius,row3-radius*1.28),(circleX+radius,row3+radius*1.28));
point(tanO,(circleX,row3));
point(tanP,(circleX+radius*cos(0.35),row3-radius*sin(0.35)));
point(tanHit,(circleX+radius,row3-radius*tan(0.35)));
point(tanBase,(circleX+radius,row3));
segment(tanRay,tanO,tanHit);
segment(tanLength,tanBase,tanHit);
bind(alpha,tanP,x,"286.2+99.4*cos(p)");
bind(alpha,tanP,y,"1430.4-99.4*sin(p)");
bind(alpha,tanHit,y,"1430.4-99.4*min(1.28,max(-1.28,tan(p)))");
bind(alpha,tanHit,opacity,"min(1,abs(cos(p))*7)");
bind(alpha,tanRay,opacity,"min(1,abs(cos(p))*7)");
bind(alpha,tanLength,opacity,"min(1,abs(cos(p))*7)");
line(tanGX,(graphX,row3),(graphRight,row3));
line(tanGY,(graphX,row3-graphSY*1.28),(graphX,row3+graphSY*1.28));
// Stop each sampled branch exactly at the visible y-range. The mathematical
// branch continues toward infinity; the empty gaps remain truthful.
plot(tanA,(graphX,row3),graphSX,graphSY,"tan(x)",(0.02,0.907));
plot(tanB,(graphX,row3),graphSX,graphSY,"tan(x)",(2.235,4.049));
plot(tanC,(graphX,row3),graphSX,graphSY,"tan(x)",(5.377,6.26));
point(tanDot,(graphX+graphSX*0.35,row3-graphSY*tan(0.35)));
point(tanFoot,(graphX+graphSX*0.35,row3));
segment(tanDrop,tanDot,tanFoot);
bind(alpha,tanA,trace,"min(1,p/0.907)");
bind(alpha,tanB,trace,"min(1,max(0,(p-2.235)/1.814))");
bind(alpha,tanC,trace,"min(1,max(0,(p-5.377)/0.883))");
bind(alpha,tanDot,x,"577.8+54*(p-6.283185*floor(p/6.283185))");
bind(alpha,tanDot,y,"1430.4-99.4*min(1.28,max(-1.28,tan(p)))");
bind(alpha,tanDot,opacity,"min(1,abs(cos(p))*7)");
bind(alpha,tanFoot,x,"577.8+54*(p-6.283185*floor(p/6.283185))");
bind(alpha,tanDrop,opacity,"min(1,abs(cos(p))*7)");
text(tanLabel,(graphX+graphSX*2.2,row3-radius*1.45),"y = tan(α) · tangent length");
color(tanCircle,fg); color(tanCX,dim); color(tanCY,dim); color(tangentWall,lime);
color(tanRay,lime); color(tanLength,lime); color(tanP,lime);
color(tanHit,fg); color(tanBase,fg); color(tanO,fg);
color(tanGX,dim); color(tanGY,dim);
color(tanA,lime); color(tanB,lime); color(tanC,lime);
color(tanDot,fg); color(tanFoot,fg); color(tanDrop,lime); color(tanLabel,lime);
dashed(tanDrop,8,6);
stroke(tanCircle,3); stroke(tanRay,4); stroke(tanLength,5);
stroke(tanA,5); stroke(tanB,5); stroke(tanC,5);
size(tanLabel,18*u); bold(tanLabel);
tag(sinCircle,trigWorld); tag(sinCX,trigWorld); tag(sinCY,trigWorld);
tag(sinO,trigWorld); tag(sinP,trigWorld); tag(sinHeight,trigWorld);
tag(sinRay,trigWorld); tag(sinGuide,trigWorld); tag(sinAngle,trigWorld);
tag(sinGX,trigWorld); tag(sinGY,trigWorld); tag(sinCurve,trigWorld);
tag(sinDot,trigWorld); tag(sinFoot,trigWorld); tag(sinDrop,trigWorld); tag(sinLabel,trigWorld);
tag(cosCircle,trigWorld); tag(cosCX,trigWorld); tag(cosCY,trigWorld);
tag(cosO,trigWorld); tag(cosP,trigWorld); tag(cosReach,trigWorld);
tag(cosRay,trigWorld); tag(cosGuide,trigWorld);
tag(cosGX,trigWorld); tag(cosGY,trigWorld); tag(cosCurve,trigWorld);
tag(cosDot,trigWorld); tag(cosFoot,trigWorld); tag(cosDrop,trigWorld); tag(cosLabel,trigWorld);
tag(tanCircle,trigWorld); tag(tanCX,trigWorld); tag(tanCY,trigWorld); tag(tangentWall,trigWorld);
tag(tanO,trigWorld); tag(tanP,trigWorld); tag(tanHit,trigWorld); tag(tanBase,trigWorld);
tag(tanRay,trigWorld); tag(tanLength,trigWorld);
tag(tanGX,trigWorld); tag(tanGY,trigWorld); tag(tanA,trigWorld); tag(tanB,trigWorld); tag(tanC,trigWorld);
tag(tanDot,trigWorld); tag(tanFoot,trigWorld); tag(tanDrop,trigWorld); tag(tanLabel,trigWorld);
hidden(kicker); hidden(head); hidden(identity); hidden(degrees); hidden(radians);
hidden(caption); hidden(alpha); hidden(trigWorld);
untraced(sinCurve); untraced(cosCurve); untraced(tanA); untraced(tanB); untraced(tanC);
step("one angle enters three worlds") {
par {
show(kicker,0.30); show(head,0.45); show(identity,0.45);
show(degrees,0.30); show(radians,0.30); show(caption,0.40);
show(alpha,0.35);
}
stagger(0.04) {
show(trigWorld,0.30);
}
}
wait(0.60);
step("height becomes sine") {
par {
to(alpha,value,1.05,2.20,smooth);
say(head,"Sine records the rotating point's height.",0.45,smooth);
say(caption,"The cyan projection and cyan graph are the same number.",0.45,smooth);
pulse(sinGuide,0.70); pulse(sinCurve,0.70);
}
}
wait(0.55);
step("reach becomes cosine") {
par {
to(alpha,value,2.35,2.30,smooth);
say(head,"Cosine records its horizontal reach.",0.45,smooth);
say(caption,"Past 90°, the point moves left—so cosine becomes negative.",0.48,smooth);
pulse(cosGuide,0.70); pulse(cosCurve,0.70);
}
}
wait(0.55);
step("tangent breaks at the asymptote") {
par {
to(alpha,value,1.56,1.70,smooth);
say(head,"Tangent races toward infinity.",0.40,smooth);
say(caption,"At π/2 the radius is parallel to the tangent line. There is no intersection.",0.50,smooth);
pulse(tangentWall,0.70);
}
cue(whoosh);
par {
to(alpha,value,1.82,1.10,smooth);
say(head,"It does not cross. A new branch begins.",0.40,smooth);
say(caption,"Manic leaves the discontinuity empty instead of drawing a false connection.",0.50,smooth);
}
}
wait(0.65);
step("one rotation reveals every sign") {
par {
to(alpha,value,3.75,3.10,smooth);
say(head,"One angle controls every sign and every value.",0.45,smooth);
say(caption,"At 215°: sine and cosine are negative, while tangent is positive.",0.50,smooth);
}
}
wait(0.70);
step("complete the cycle") {
par {
to(alpha,value,6.25,4.00,smooth);
say(head,"Three graphs. One rotating radius.",0.45,smooth);
say(caption,"The unit circle is not a mnemonic—it is the machine generating all three functions.",0.52,smooth);
}
}
step("keep the mathematics running") {
par {
// No hold between cycles: the angle keeps moving while the camera turns
// three stacked diagrams into one continuous cinematic inspection.
to(alpha,value,12.53,7.60,linear);
fade(alpha,0.30);
say(head,"Now let the mathematics keep running.",0.45,smooth);
say(caption,"The camera can move because every relationship remains alive.",0.48,smooth);
seq {
par {
cam((cx,row1),1.00,smooth);
zoom(1.28,1.00,smooth);
}
wait(1.00);
cam((cx,row2),1.00,smooth);
wait(1.00);
par {
// Aim slightly above the tangent row so its graph, explanation, and
// creator identity share the frame instead of leaving a dead lower half.
cam((cx,h*0.50),1.00,smooth);
zoom(1.24,1.00,smooth);
}
wait(1.40);
par {
cam((cx,cy),1.20,smooth);
zoom(1.00,1.20,smooth);
}
}
}
}
step("creator call to action") {
par {
fade(kicker,0.30); fade(head,0.30); fade(identity,0.30);
fade(degrees,0.30); fade(radians,0.30); fade(caption,0.30);
fade(alpha,0.30); fade(trigWorld,0.45); fade(me.footer,0.30);
show(me.endcard,0.60);
}
}
wait(2.00);
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);
sqrt-tanx
√(tan θ) by pure geometry — a 3B1B-style proof: on the unit circle, tan θ is the vertical tangent length; Euclid’s geometric mean (semicircle on diameter 1+a, altitude at 1) turns that length into √(tan θ). Then the curve itself, painted teal → violet → coral. Uses the expanded semantic palette.
// ============================================================================
// sqrt-tanx.manic — √(tan x) by PURE GEOMETRY (3B1B-style)
// ----------------------------------------------------------------------------
// THE PROBLEM: √(tan θ) looks like algebra. It is geometry.
//
// THE PROOF, in three pictures:
// 1. On the unit circle, tan θ IS the length of the vertical tangent segment.
// 2. √a is the geometric mean of 1 and a — Euclid: semicircle on diameter
// 1+a, erect a perpendicular at the "1" mark; its height is √a.
// 3. Set a = tan θ. The altitude IS √(tan θ). No algebra required.
//
// Then we sweep θ and watch the construction's height paint the curve —
// teal → violet → coral — using the expanded semantic palette.
//
// manic examples/sqrt-tanx.manic
// manic examples/sqrt-tanx.manic --still 22
// ============================================================================
title("√(tan x) — Pure Geometry");
canvas("16:9");
template("blank");
// ---- stage ----
circle(well, (cx, 400), 340);
filled(well);
gradient(well, panel, void, radial);
opacity(well, 0.55);
// ============================================================================
// LEFT — the unit circle, where tan θ is a length
// ============================================================================
let ox = 250;
let oy = 430;
let R = 145;
// a friendly angle: θ = 0.95 rad ≈ 54.4°, tan ≈ 1.40, √tan ≈ 1.18
let th = 0.95;
let Px = ox + R*cos(th);
let Py = oy - R*sin(th);
let Ty = oy - R*tan(th);
circle(O, (ox, oy), 7); outlined(O); color(O, fg); stroke(O, 1.5); hidden(O);
dot(A, (ox + R, oy), 5); color(A, dim); hidden(A); // (1, 0)
// contact points: open dashed rings (filled dots hide the circle + tangent)
circle(P, (Px, Py), 11);
outlined(P); color(P, mint); stroke(P, 2); dashed(P, 3.5, 2.8); untraced(P); hidden(P);
circle(T, (ox + R, Ty), 11);
outlined(T); color(T, coral); stroke(T, 2); dashed(T, 3.5, 2.8); untraced(T); hidden(T);
// ray as a line (not arrow) so the filled arrowhead doesn't cover P
line(ray, (ox, oy), (Px, Py));
color(ray, mint); stroke(ray, 2.5); untraced(ray); hidden(ray);
// the tangent SEGMENT — its length IS tan θ (in radii)
line(tanseg, (ox + R, oy), (ox + R, Ty));
color(tanseg, coral); stroke(tanseg, 5); untraced(tanseg); hidden(tanseg);
// angle mark — textbook style: a thin ARC, never a filled wedge (fills hide the diagram)
arc(wedge, (ox, oy), R*0.42, 0, -th*180/pi);
color(wedge, violet); stroke(wedge, 2.5); untraced(wedge); hidden(wedge);
circle(uc, (ox, oy), R);
outlined(uc); outline(uc, teal); stroke(uc, 3); untraced(uc); hidden(uc);
line(axX, (ox - R - 30, oy), (ox + R + 100, oy));
color(axX, dim); stroke(axX, 1.5); untraced(axX); hidden(axX);
line(axY, (ox, oy + R + 40), (ox, oy - R - 50));
color(axY, dim); stroke(axY, 1.5); untraced(axY); hidden(axY);
line(tline, (ox + R, oy + R + 50), (ox + R, oy - R - 90));
color(tline, dim); stroke(tline, 1.5); dashed(tline, 10, 8); untraced(tline); hidden(tline);
text(labP, (Px + 14, Py - 14), "P"); size(labP, 20); color(labP, mint); hidden(labP);
text(labO, (ox - 18, oy + 22), "O"); size(labO, 18); color(labO, dim); hidden(labO);
text(labA, (ox + R + 14, oy + 22), "1"); size(labA, 18); color(labA, dim); hidden(labA);
text(labT, (ox + R + 18, Ty), "tan θ"); size(labT, 20); color(labT, coral); hidden(labT);
text(labTh, (ox + 48, oy - 28), "θ"); size(labTh, 22); color(labTh, violet); hidden(labTh);
// ============================================================================
// RIGHT — Euclid's geometric mean: √a from 1 and a
// ============================================================================
let gx = 620;
let gy = 500;
let u = 110; // px per unit
let a = tan(th); // ≈ 1.40
let root = sqrt(a); // ≈ 1.18
let G0x = gx;
let G1x = gx + u; // the "1" mark
let G2x = gx + u*(1 + a); // the "1+a" mark
let mid = (G0x + G2x)*0.5;
let rad = u*(1 + a)*0.5;
dot(G0, (G0x, gy), 5); color(G0, dim); hidden(G0);
dot(G1, (G1x, gy), 6); color(G1, teal); glow(G1, 0.4); hidden(G1);
dot(G2, (G2x, gy), 5); color(G2, coral); hidden(G2);
line(base, (G0x, gy), (G2x, gy));
color(base, fg); stroke(base, 3); untraced(base); hidden(base);
// upper semicircle on diameter [0, 1+a]
// screen angles: 0° right, 90° down, 270° up — from left through UP: start 180, sweep +180
arc(semi, (mid, gy), rad, 180, 180);
color(semi, indigo); stroke(semi, 3); untraced(semi); hidden(semi);
// altitude at the "1" mark — height = √(1·a) = √(tan θ)
line(alt, (G1x, gy), (G1x, gy - root*u));
color(alt, violet); stroke(alt, 5); untraced(alt); hidden(alt);
dot(H, (G1x, gy - root*u), 8); color(H, violet); glow(H, 0.7); hidden(H);
text(lab0, (G0x, gy + 28), "0"); size(lab0, 18); color(lab0, dim); hidden(lab0);
text(lab1, (G1x, gy + 28), "1"); size(lab1, 18); color(lab1, teal); hidden(lab1);
text(labA2, (G2x, gy + 28), "1+tanθ"); size(labA2, 18); color(labA2, coral); hidden(labA2);
text(labH, (G1x + 16, gy - root*u - 8), "√(tan θ)"); size(labH, 22); color(labH, violet); hidden(labH);
// brace / connector: coral tan length → feeds the geometric mean
arrow(feed, (ox + R + 36, (oy + Ty)*0.5), (G2x - 10, gy - 40));
color(feed, coral); stroke(feed, 2); dashed(feed, 8, 6); untraced(feed); hidden(feed);
// ============================================================================
// BOTTOM — the curve itself, colored by the new palette
// ============================================================================
axes(ax, (920, 400), 280, 220);
hidden(ax);
plot(curve, (920, 400), 95, 85, "sqrt(tan(x))", (0.25, 1.35));
stroke(curve, 5); untraced(curve); hidden(curve);
gradient(curve, teal, violet, coral, 270);
// low x → small √tan (teal); high x → large (coral), via height = truth
// ============================================================================
// TYPOGRAPHY
// ============================================================================
caption(q, "what IS the square root of a tangent?", (cx, 58), 32, gold);
equation(law, (cx, 110), `\sqrt{\tan\theta}`, 44);
color(law, violet); hidden(law);
equation(mean, (900, 160), `\sqrt{a}=\sqrt{1\cdot a}`, 28);
color(mean, teal); hidden(mean);
text(cap, (cx, h - 36), ""); size(cap, 20); color(cap, dim); display(cap);
// ============================================================================
// SCRIPT
// ============================================================================
// 1 — HOOK
say(cap, "an expression that looks like algebra — but it is a length", 0.4);
wordpop(q, 0.16);
wait(0.3);
show(law, 0.5);
wait(0.6);
// 2 — UNIT CIRCLE
say(cap, "start with the unit circle and an angle θ", 0.4);
par { show(axX, 0.05); show(axY, 0.05); show(uc, 0.05); show(tline, 0.05); }
par {
draw(axX, 0.5); draw(axY, 0.5);
draw(uc, 1.0);
}
par { show(O, 0.3); show(labO, 0.3); }
draw(tline, 0.5);
show(A, 0.3); show(labA, 0.3);
wait(0.3);
// 3 — THE ANGLE AND THE RAY
say(cap, "draw the ray at angle θ — it meets the circle at P", 0.4);
show(wedge, 0.05); draw(wedge, 0.5); show(labTh, 0.3);
show(ray, 0.05); draw(ray, 0.7);
show(P, 0.05); draw(P, 0.35); show(labP, 0.3);
pulse(P);
wait(0.4);
// 4 — TAN θ IS A LENGTH
say(cap, "extend to the vertical tangent line — that segment's length IS tan θ", 0.4);
show(tanseg, 0.05); draw(tanseg, 0.9);
show(T, 0.05); draw(T, 0.35); show(labT, 0.3);
flash(tanseg, coral);
wait(0.7);
// 5 — GEOMETRIC MEAN
say(cap, "now forget the circle — Euclid: √a is the geometric mean of 1 and a", 0.4);
show(mean, 0.5);
par {
show(G0, 0.3); show(G1, 0.3); show(G2, 0.3);
show(lab0, 0.3); show(lab1, 0.3); show(labA2, 0.3);
show(base, 0.05);
}
draw(base, 0.6);
wait(0.3);
say(cap, "semicircle on diameter 1 + tan θ — then erect a perpendicular at 1", 0.4);
show(semi, 0.05); draw(semi, 1.2);
show(alt, 0.05); draw(alt, 0.9);
show(H, 0.3); show(labH, 0.3);
flash(alt, violet);
wait(0.5);
say(cap, "by Thales + similar triangles, that height equals √(1 · tan θ)", 0.4);
show(feed, 0.05); draw(feed, 0.7);
pulse(H); pulse(T);
wait(0.8);
// 6 — THE IDENTITY
recolor(cap, violet);
say(cap, "so the altitude IS √(tan θ) — the symbol was a length all along", 0.4);
rewrite(law, `\sqrt{\tan\theta}=h`, 0.8, smooth);
wait(0.9);
// 7 — THE CURVE
recolor(cap, dim);
say(cap, "every such height, for every θ, paints the curve √(tan x)", 0.4);
show(ax, 0.5);
show(curve, 0.05); draw(curve, 2.2);
wait(0.4);
say(cap, "teal near zero, coral as tan θ climbs toward the asymptote at π/2", 0.4);
flash(curve, violet);
wait(0.8);
// 8 — POINT
recolor(cap, teal);
say(cap, "√(tan θ) is not a formula to memorize — it is a geometric construction", 0.4);
wait(1.6);
integral-sqrt-tanx
Integrate √(tan P): substitute P = t + π/4, read A(t) from the circle x²+y²=2 and H(t) from the hyperbola x²−y²=2, then dA−dH recovers the integrand. Outline-only angle marks — no filled discs hiding the construction.
// ============================================================================
// integral-sqrt-tanx.manic — ∫ √(tan P) dP by PURE GEOMETRY
// Companion to sqrt-tanx.manic. Storyboard: the handwritten circle+hyperbola
// derivation (x²+y²=2 meets x²−y²=2; A(t)−H(t) recovers the integrand).
// ----------------------------------------------------------------------------
// THE PROBLEM: √(tan P) is a length. What is its antiderivative?
//
// THE GEOMETRY (one diagram, two curves of "radius" √2):
// · circle x² + y² = 2 → A(t) = arcsin(√2 sin t)
// · hyperbola x² − y² = 2 → H(t) = arcosh(√2 cos t)
// · substitution P = t + π/4 (so t ∈ (0, π/4) ⇒ P ∈ (π/4, π/2))
//
// THE TRUTH:
// dA − dH = √2 · √(tan P) · dP
// therefore
// ∫ √(tan P) dP = (1/√2)(A(t) − H(t)) + C
//
// Colours (expanded palette): teal = circle/A, coral = hyperbola/H,
// violet = the integral, mint = the moving point, gold = the hook.
//
// manic examples/integral-sqrt-tanx.manic
// manic examples/integral-sqrt-tanx.manic --still 20
// ============================================================================
title("∫ √(tan P) dP — Circle Meets Hyperbola");
canvas("16:9");
template("blank");
// ---- stage ----
circle(well, (cx, 400), 360);
filled(well);
gradient(well, panel, void, radial);
opacity(well, 0.5);
// ============================================================================
// THE DIAGRAM — circle x²+y²=2 and hyperbola x²−y²=2
// Screen: origin at (ox,oy), scale s px per unit. Radius √2 ≈ 1.414.
// ============================================================================
let ox = 340;
let oy = 400;
let s = 130; // px per math-unit
let R = s*sqrt(2); // circle radius = √2
// a friendly t inside (0, π/4): t = 0.40 rad ≈ 23°
let t = 0.40;
let P = t + pi/4; // ≈ 1.185 rad ≈ 68°
// point C on the circle: (√2 cos t, √2 sin t)
let Cx = ox + s*sqrt(2)*cos(t);
let Cy = oy - s*sqrt(2)*sin(t);
// foot on the x-axis and the "hyperbola x" = √2 cosh u with cosh u = √2 cos t?
// From notes: X = √2 cos t = cosh H, so the hyperbola vertex is at x=√2.
// Point on right branch with x = √2 cos t... wait cosh H = √2 cos t, so
// x_hyp = cosh H = √2 cos t (in math units where a=1 for cosh param form
// of x²−y²=1). Our hyperbola is x²−y²=2 = (x/√2)² − (y/√2)², so
// parametric: x = √2 cosh u, y = √2 sinh u. And cosh H = √2 cos t? Notes say
// cosh(H) = X = √2 cos t — that would require cosh H ≤ √2, H = arcosh(√2 cos t).
// For the diagram, mark the circle point and the matching hyperbola x.
// axes
line(axX, (ox - R - 40, oy), (ox + R + 120, oy));
color(axX, dim); stroke(axX, 1.5); untraced(axX); hidden(axX);
line(axY, (ox, oy + R + 50), (ox, oy - R - 60));
color(axY, dim); stroke(axY, 1.5); untraced(axY); hidden(axY);
// circle x² + y² = 2 — OUTLINED only. `circle` defaults to filled; a solid
// disc hides axes and labels underneath (engine-test: never fill constructions).
circle(circ, (ox, oy), R);
outlined(circ); outline(circ, teal); stroke(circ, 3.5); untraced(circ); hidden(circ);
// hyperbola x² − y² = 2 → a = b = √2 (in math units) → a_px = R
hyperbola(hyp, (ox, oy), R, R, 1.15);
color(hyp, coral); stroke(hyp, 3); untraced(hyp);
hidden(hyp.r); hidden(hyp.l);
// origin: thin open ring (tiny r + thick stroke reads as a filled blob)
circle(O, (ox, oy), 7);
outlined(O); color(O, fg); stroke(O, 1.5); hidden(O);
// contact point C: dashed open circle so circle + projections show through.
// Keep radius >> stroke so the interior stays empty (textbook marker).
circle(C, (Cx, Cy), 12);
outlined(C); color(C, mint); stroke(C, 2); dashed(C, 3.5, 2.8); untraced(C); hidden(C);
// radius toward C — a LINE, not an arrow: arrowheads are filled discs and
// hide the open marker at the contact point (engine-test visibility rule)
line(ray, (ox, oy), (Cx, Cy));
color(ray, mint); stroke(ray, 2.5); untraced(ray); hidden(ray);
// horizontal from C to y-axis? notes have Y = √2 sin t as height
line(ht, (ox, Cy), (Cx, Cy));
color(ht, violet); stroke(ht, 2); dashed(ht, 8, 6); untraced(ht); hidden(ht);
// vertical drop to x-axis
line(vt, (Cx, oy), (Cx, Cy));
color(vt, teal); stroke(vt, 2); dashed(vt, 8, 6); untraced(vt); hidden(vt);
// angle mark — textbook style: a thin ARC, never a filled disc (fills hide axes)
arc(wedge, (ox, oy), R*0.38, 0, -t*180/pi);
color(wedge, violet); stroke(wedge, 2.5); untraced(wedge); hidden(wedge);
// labels on the diagram
text(labO, (ox - 16, oy + 22), "O"); size(labO, 18); color(labO, dim); hidden(labO);
text(labC, (Cx + 22, Cy - 22), "C(t)"); size(labC, 20); color(labC, mint); hidden(labC);
text(labT, (ox + 42, oy - 22), "t"); size(labT, 22); color(labT, violet); hidden(labT);
text(labCirc, (ox - 20, oy - R - 24), "x² + y² = 2"); size(labCirc, 20); color(labCirc, teal); hidden(labCirc);
text(labHyp, (ox + R + 55, oy + 40), "x² − y² = 2"); size(labHyp, 20); color(labHyp, coral); hidden(labHyp);
// region hint: dashed circle (not a filled disc, not just a short arc)
circle(zone, (ox + R*0.42, oy - R*0.18), R*0.38);
outlined(zone); color(zone, indigo); stroke(zone, 2); dashed(zone, 7, 5);
untraced(zone); hidden(zone);
// ============================================================================
// RIGHT — the algebraic spine, rewritten live
// ============================================================================
equation(eqP, (920, 150), `P=t+\tfrac{\pi}{4}`, 32);
color(eqP, gold); hidden(eqP);
equation(eqA, (920, 230), `A(t)=\sin^{-1}(\sqrt{2}\sin t)`, 28);
color(eqA, teal); hidden(eqA);
equation(eqH, (920, 300), `H(t)=\cosh^{-1}(\sqrt{2}\cos t)`, 28);
color(eqH, coral); hidden(eqH);
equation(eqD, (920, 390), `dA-dH=\sqrt{2}\,\sqrt{\tan P}\,dP`, 28);
color(eqD, violet); hidden(eqD);
equation(eqI, (920, 480), `\int\sqrt{\tan P}\,dP=\tfrac{1}{\sqrt{2}}(A-H)+C`, 30);
color(eqI, mint); hidden(eqI);
// final closed form (boxed punchline)
equation(eqF, (920, 580), `\tfrac{1}{\sqrt{2}}\Big(\sin^{-1}(\sqrt{2}\sin(P-\tfrac{\pi}{4}))-\cosh^{-1}(\sqrt{2}\cos(P-\tfrac{\pi}{4}))\Big)+C`, 22);
color(eqF, gold); hidden(eqF);
// ============================================================================
// THE CURVE — √(tan x) on (π/4, ~1.4), then its antiderivative via ln-form of acosh
// ============================================================================
axes(ax, (920, 400), 300, 240);
hidden(ax);
// integrand (same spirit as the companion film)
plot(integrand, (920, 400), 90, 70, "sqrt(tan(x))", (0.85, 1.45));
stroke(integrand, 4); untraced(integrand); hidden(integrand);
gradient(integrand, teal, violet, coral, 270);
// antiderivative F(P) = (1/√2)(asin(√2 sin(P-π/4)) - acosh(√2 cos(P-π/4)))
// acosh(u) = ln(u + sqrt(u*u - 1))
plot(anti, (920, 400), 90, 55,
"(asin(sqrt(2)*sin(x-pi/4)) - ln(sqrt(2)*cos(x-pi/4) + sqrt(2*cos(x-pi/4)*cos(x-pi/4) - 1)))/sqrt(2)",
(0.85, 1.45));
stroke(anti, 4); untraced(anti); hidden(anti);
gradient(anti, mint, indigo, violet, 270);
// ============================================================================
// TYPOGRAPHY
// ============================================================================
caption(q, "how do you integrate a square root of a tangent?", (cx, 52), 30, gold);
equation(hook, (cx, 105), `\int\sqrt{\tan P}\,dP`, 42);
color(hook, violet); hidden(hook);
text(cap, (cx, h - 34), ""); size(cap, 20); color(cap, dim); display(cap);
// ============================================================================
// SCRIPT
// ============================================================================
// 1 — HOOK
say(cap, "√(tan P) is a length — we proved that. Now integrate it.", 0.4);
wordpop(q, 0.14);
wait(0.25);
show(hook, 0.5);
wait(0.6);
// 2 — THE SUBSTITUTION
say(cap, "one substitution opens the geometry: P = t + π/4", 0.4);
show(eqP, 0.5);
flash(eqP, gold);
wait(0.7);
// 3 — THE STAGE: two curves of the same "2"
say(cap, "draw two curves that share the constant 2 — a circle and a hyperbola", 0.4);
par { show(axX, 0.05); show(axY, 0.05); }
par { draw(axX, 0.4); draw(axY, 0.4); }
show(circ, 0.05); draw(circ, 1.0);
show(labCirc, 0.3); show(O, 0.3); show(labO, 0.3);
wait(0.3);
show(hyp.r, 0.05); show(hyp.l, 0.05);
draw(hyp.r, 1.0); draw(hyp.l, 1.0);
show(labHyp, 0.3);
wait(0.5);
say(cap, "circle: x² + y² = 2. hyperbola: x² − y² = 2. Same 2 — intentional.", 0.4);
pulse(circ); pulse(hyp.r);
wait(0.7);
// 4 — THE MOVING POINT
say(cap, "park a point C(t) = (√2 cos t, √2 sin t) on the circle — t < π/4", 0.4);
show(wedge, 0.05); draw(wedge, 0.45); show(labT, 0.3);
show(ray, 0.05); draw(ray, 0.7);
show(C, 0.05); draw(C, 0.4); show(labC, 0.3);
show(ht, 0.05); show(vt, 0.05);
draw(ht, 0.5); draw(vt, 0.5);
show(zone, 0.05); draw(zone, 0.6);
wait(0.6);
// 5 — A(t) FROM THE CIRCLE
say(cap, "from the y-coordinate: A(t) = arcsin(√2 sin t) — an angle on that circle", 0.4);
show(eqA, 0.5);
flash(eqA, teal);
wait(0.8);
// 6 — H(t) FROM THE HYPERBOLA
say(cap, "from the x-coordinate: H(t) = arcosh(√2 cos t) — a hyperbolic angle", 0.4);
show(eqH, 0.5);
flash(eqH, coral);
wait(0.8);
// 7 — THE DIFFERENTIAL IDENTITY
say(cap, "differentiate both — their difference IS the integrand", 0.4);
show(eqD, 0.6);
flash(eqD, violet);
wait(0.5);
say(cap, "dA − dH = √2 · √(tan P) · dP — geometry differentiated", 0.4);
wait(1.0);
// 8 — INTEGRATE
recolor(cap, mint);
say(cap, "integrate both sides — the √2 cancels into the prefactor", 0.4);
show(eqI, 0.6);
pulse(eqI);
wait(0.9);
// 9 — CLOSED FORM
recolor(cap, violet);
say(cap, "substitute t = P − π/4 back — the antiderivative in closed form", 0.4);
show(eqF, 0.7);
flash(eqF, violet);
wait(1.0);
// 10 — SEE IT: integrand then antiderivative
recolor(cap, dim);
say(cap, "the integrand √(tan x) climbing toward π/2", 0.4);
par {
fade(eqA, 0.35); fade(eqH, 0.35); fade(eqD, 0.35); fade(eqI, 0.35); fade(eqF, 0.35);
fade(eqP, 0.35);
}
show(ax, 0.4);
show(integrand, 0.05); draw(integrand, 1.8);
wait(0.5);
say(cap, "and its antiderivative — (1/√2)(A − H) — the circle minus the hyperbola", 0.4);
show(anti, 0.05); draw(anti, 2.0);
flash(anti, mint);
wait(0.8);
// 11 — POINT
recolor(cap, teal);
say(cap, "a hard integral, solved because two curves of the same 2 were waiting", 0.4);
wait(1.8);
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);
iit-exp-equation
A JEE Advanced exponential equation solved by REWRITE, then LOOK: 4^x+6^{x^2}=5^x+5^{x^2} regroups to 6^{x^2}-5^{x^2}=5^x-4^x; the two sides trace in, meet only at x=0 and x=1, and the Mean Value Theorem reveals the hidden parabola x^2-x whose sign IS the equation’s sign — so it is secretly x^2=x. Typeset captions; the problem lands centre-stage, then docks.
// ============================================================================
// iit-exp-equation.manic — a JEE Advanced exponential equation, solved with
// DEPTH and MOTION (curves trace in; the hidden skeleton is revealed).
// 4^x + 6^{x^2} = 5^x + 5^{x^2} -> 6^{x^2}-5^{x^2} = 5^x-4^x
// RIGOUR (MVT, math.stackexchange/2079546):
// 6^{x^2}-5^{x^2} = x^2 * xi^{x^2-1} (xi in (5,6)),
// 5^x-4^x = x * eta^{x-1} (eta in (4,5)),
// so LHS-RHS = x^2*xi^{x^2-1} - x*eta^{x-1}, and bounding by the middle
// base 5 gives sign(LHS-RHS) = sign(x^2 - x). Hence LHS=RHS <=> x^2=x.
// Captions are LaTeX equations (typeset math, not ASCII), shown one at a time.
// ============================================================================
title("JEE — 4^x + 6^{x^2} = 5^x + 5^{x^2}");
canvas(1280, 720);
template("black");
text(brow, (640, 40), "JEE ADVANCED · HOW MANY REAL SOLUTIONS?");
color(brow, dim); size(brow, 18); bold(brow); hidden(brow);
equation(eq0, (640, 360), `4^{x}+6^{x^{2}}=5^{x}+5^{x^{2}}`, 40); // lands CENTRE, big
equation(eq1, (640, 150), `6^{x^{2}}-5^{x^{2}}\;=\;5^{x}-4^{x}`, 26);
color(eq0, fg); color(eq1, gold); hidden(eq0); hidden(eq1);
// ---- captions: typeset LaTeX (one shown at a time), bottom of frame ----
equation(c1, (640, 690), `\text{Bases }4,5,6\ \text{· exponents }x,\ x^{2}\ \text{· how many real roots?}`, 22);
equation(c2, (640, 690), `\text{Regroup: put the }x^{2}\text{-powers together, the }x\text{-powers together}`, 22);
equation(c3, (640, 690), `L(x)=6^{x^{2}}-5^{x^{2}}\ \ge\ 0\quad(\text{even U, zero only at }x=0)`, 22);
equation(c4, (640, 690), `R(x)=5^{x}-4^{x}\quad\text{takes the sign of }x`, 22);
equation(c5, (640, 690), `\text{They meet at }x=0\text{ and }x=1:\quad 4+6=10=5+5`, 22);
equation(c6, (640, 690), `\text{MVT: } L-R=x^{2}\xi^{\,x^{2}-1}-x\,\eta^{\,x-1},\quad \operatorname{sign}=\operatorname{sign}(x^{2}-x)`, 21);
equation(c7, (640, 690), `\text{A parabola hides inside:}\quad x^{2}-x=x(x-1)`, 22);
equation(c8, (640, 690), `x^{2}-x<0\text{ on }(0,1)\Rightarrow L<R;\quad\text{outside, } L>R`, 22);
equation(c9, (640, 690), `\text{Secretly } x^{2}=x\ \Rightarrow\ \text{two roots, no more}`, 22);
color(c1,dim); color(c2,dim); color(c3,dim); color(c4,dim); color(c5,dim);
color(c6,dim); color(c7,dim); color(c8,dim); color(c9,dim);
hidden(c1); hidden(c2); hidden(c3); hidden(c4); hidden(c5);
hidden(c6); hidden(c7); hidden(c8); hidden(c9);
// ---- axes: math origin at pixel (455,452); 270 px/x-unit, 100 px/y-unit ----
plane(pl, (455, 452), 380, 228, 100);
hidden(pl.grid);
untraced(pl.x); untraced(pl.y);
// the two sides of the rewritten equation, and the HIDDEN skeleton x^2 - x
plot(Lc, (455, 452), 270, 100, "6^(x*x) - 5^(x*x)", 1.35);
plot(Rc, (455, 452), 270, 100, "5^x - 4^x", 1.35);
plot(Pc, (455, 452), 270, 100, "x*x - x", 1.35);
color(Lc, gold); color(Rc, cyan); color(Pc, magenta);
stroke(Lc, 2.6); stroke(Rc, 2.6); stroke(Pc, 2.6);
untraced(Lc); untraced(Rc); untraced(Pc); // start un-drawn; draw() traces them
// the negative dip of the skeleton on (0,1) — swept open for effect
area(dip, Pc, 0, 0); color(dip, magenta); hidden(dip);
// a guide at x=1 tying the (1,1) crossing down to the parabola's root at (1,0)
line(g1, (725, 250), (725, 512)); color(g1, dim); stroke(g1, 1.4); untraced(g1);
// labels + ticks
equation(Llab, (210, 246), `6^{x^{2}}-5^{x^{2}}`, 19); color(Llab, gold); hidden(Llab);
equation(Rlab, (760, 300), `5^{x}-4^{x}`, 19); color(Rlab, cyan); hidden(Rlab);
equation(Plab, (748, 505), `x^{2}-x`, 19); color(Plab, magenta); hidden(Plab);
text(tx1, (725, 474), "1"); color(tx1, dim); size(tx1, 18); hidden(tx1);
text(ty1, (435, 350), "1"); color(ty1, dim); size(ty1, 18); hidden(ty1);
// intersections: math (0,0)->px(455,452); (1,1)->px(725,352)
point(P0, (455, 452), "x = 0"); color(P0, lime); hidden(P0);
point(P1, (725, 352), "x = 1"); color(P1, orange); hidden(P1);
equation(ans, (1048, 300), `\boxed{\,x=0\ \ \text{or}\ \ x=1\,}`, 28);
color(ans, lime); hidden(ans);
// ============================ the lesson ====================================
step("the problem — front and centre") {
show(eq0, 0.6);
show(c1, 0.4);
}
wait(1.6);
step("dock it to the top") {
show(brow, 0.4);
par { move(eq0, (640, 96), 0.9, smooth); scale(eq0, 0.78, 0.9); }
}
wait(0.3);
step("one rewrite") {
par { fade(c1, 0.25); show(c2, 0.3); }
show(eq1, 0.6);
}
wait(1.5);
step("axes") {
par { draw(pl.x, 0.5); draw(pl.y, 0.5); }
show(tx1, 0.3); show(ty1, 0.3);
}
wait(0.3);
step("trace the left side") {
par { fade(c2, 0.25); show(c3, 0.3); }
draw(Lc, 1.2);
show(Llab, 0.4);
}
wait(1.5);
step("trace the right side") {
par { fade(c3, 0.25); show(c4, 0.3); }
draw(Rc, 1.2);
show(Rlab, 0.4);
}
wait(1.5);
step("two crossings") {
par { fade(c4, 0.25); show(c5, 0.3); }
show(P0, 0.4); show(P1, 0.4);
pulse(P0, 0.5); pulse(P1, 0.5);
}
wait(1.6);
step("the rigorous why (MVT)") {
par { fade(c5, 0.25); show(c6, 0.3); }
}
wait(2.0);
step("the hidden skeleton") {
par { fade(c6, 0.25); show(c7, 0.3); }
draw(Pc, 1.2);
show(Plab, 0.4);
draw(g1, 0.6);
}
wait(1.6);
step("sign follows the parabola") {
par { fade(c7, 0.25); show(c8, 0.3); }
show(dip, 0.2);
to(dip, x, 1, 1.5);
}
wait(1.7);
step("the answer") {
par { fade(c8, 0.25); show(c9, 0.3); }
show(ans, 0.6);
pulse(ans, 0.6);
}
wait(2.6);
iit-exp-equation-short
The 9:16 Short cut of the JEE exponential-equation proof: same rewrite -> trace -> MVT -> hidden x^2-x skeleton -> answer, laid out vertically with a CTA endcard.
// ============================================================================
// iit-exp-equation-short.manic — the 9:16 Short cut, with DEPTH + MOTION.
// Same proof as the landscape iit-exp-equation.manic (MVT + the hidden x^2-x
// skeleton, math.stackexchange/2079546), re-laid out for vertical:
// 4^x + 6^{x^2} = 5^x + 5^{x^2} -> 6^{x^2}-5^{x^2} = 5^x-4^x
// Curves TRACE in; MVT => sign(L-R) = sign(x^2-x); the parabola x(x-1) hides
// inside, roots 0 and 1 only. Captions are typeset LaTeX equations.
// ============================================================================
title("JEE exponential equation — how many real solutions?");
canvas(1080, 1920);
template("black");
watermark(mark, (w*0.16, h*0.03), "Made With Manic");
text(brow, (540, 96), "JEE ADVANCED · HOW MANY REAL SOLUTIONS?");
color(brow, dim); size(brow, 26); bold(brow); hidden(brow);
equation(eq0, (540, 900), `4^{x}+6^{x^{2}}=5^{x}+5^{x^{2}}`, 52); // lands CENTRE, big
equation(eq1, (540, 320), `6^{x^{2}}-5^{x^{2}}\;=\;5^{x}-4^{x}`, 34);
color(eq0, fg); color(eq1, gold); hidden(eq0); hidden(eq1);
// ---- captions: typeset LaTeX (one shown at a time), bottom of frame ----
equation(c1, (540, 1690), `\text{Bases }4,5,6\ \text{· exponents }x,\ x^{2}`, 24);
equation(c2, (540, 1690), `\text{Regroup: }x^{2}\text{-powers vs }x\text{-powers}`, 24);
equation(c3, (540, 1690), `L(x)=6^{x^{2}}-5^{x^{2}}\ \ge\ 0\ \ (\text{zero at }x=0)`, 24);
equation(c4, (540, 1690), `R(x)=5^{x}-4^{x}\ \ \text{has the sign of }x`, 24);
equation(c5, (540, 1690), `\text{Meet at }x=0,\,x=1:\ \ 4+6=10=5+5`, 24);
equation(c6, (540, 1690), `\text{MVT}\Rightarrow\operatorname{sign}(L-R)=\operatorname{sign}(x^{2}-x)`, 24);
equation(c7, (540, 1690), `\text{Hidden skeleton:}\ \ x^{2}-x=x(x-1)`, 24);
equation(c8, (540, 1690), `x^{2}-x<0\text{ on }(0,1)\ \Rightarrow\ L<R`, 24);
equation(c9, (540, 1690), `\text{Secretly } x^{2}=x\ \Rightarrow\ \text{two roots}`, 24);
color(c1,dim); color(c2,dim); color(c3,dim); color(c4,dim); color(c5,dim);
color(c6,dim); color(c7,dim); color(c8,dim); color(c9,dim);
hidden(c1); hidden(c2); hidden(c3); hidden(c4); hidden(c5);
hidden(c6); hidden(c7); hidden(c8); hidden(c9);
text(cta, (540, 1806), "Try it → 8gwifi.org/manic"); color(cta, cyan); size(cta, 30); bold(cta); hidden(cta);
// ---- tall plot: math origin px(500,1120); 300 px/x-unit, 180 px/y-unit ----
plane(pl, (500, 1120), 410, 350, 180);
hidden(pl.grid);
untraced(pl.x); untraced(pl.y);
plot(Lc, (500, 1120), 300, 180, "6^(x*x) - 5^(x*x)", 1.18);
plot(Rc, (500, 1120), 300, 180, "5^x - 4^x", 1.18);
plot(Pc, (500, 1120), 300, 180, "x*x - x", 1.18);
color(Lc, gold); color(Rc, cyan); color(Pc, magenta);
stroke(Lc, 3.2); stroke(Rc, 3.2); stroke(Pc, 3.0);
untraced(Lc); untraced(Rc); untraced(Pc); // trace in with draw()
// the parabola's negative dip on (0,1) — swept open for effect
area(dip, Pc, 0, 0); color(dip, magenta); hidden(dip);
// guide at x=1 tying the (1,1) crossing down to the parabola's root at (1,0)
line(g1, (800, 900), (800, 1170)); color(g1, dim); stroke(g1, 1.6); untraced(g1);
equation(Llab, (232, 760), `6^{x^{2}}-5^{x^{2}}`, 26); color(Llab, gold); hidden(Llab);
equation(Rlab, (836, 1000), `5^{x}-4^{x}`, 26); color(Rlab, cyan); hidden(Rlab);
equation(Plab, (700, 1214), `x^{2}-x`, 26); color(Plab, magenta); hidden(Plab);
text(tx1, (800, 1092), "1"); color(tx1, dim); size(tx1, 22); hidden(tx1);
text(ty1, (462, 936), "1"); color(ty1, dim); size(ty1, 22); hidden(ty1);
// intersections: math (0,0)->px(500,1120); (1,1)->px(800,940)
point(P0, (500, 1120), "x = 0"); color(P0, lime); hidden(P0);
point(P1, (800, 940), "x = 1"); color(P1, orange); hidden(P1);
equation(ans, (540, 1560), `\boxed{\,x=0\ \ \text{or}\ \ x=1\,}`, 40);
color(ans, lime); hidden(ans);
// ============================ the Short ====================================
step("the problem — front and centre") {
show(eq0, 0.6);
show(c1, 0.4);
}
wait(1.6);
step("dock it to the top") {
show(brow, 0.4);
par { move(eq0, (540, 214), 0.9, smooth); scale(eq0, 0.80, 0.9); }
}
wait(0.3);
step("one rewrite") {
par { fade(c1, 0.25); show(c2, 0.3); }
show(eq1, 0.6);
}
wait(1.5);
step("axes") {
par { draw(pl.x, 0.5); draw(pl.y, 0.5); }
show(tx1, 0.3); show(ty1, 0.3);
}
wait(0.3);
step("trace the left side") {
par { fade(c2, 0.25); show(c3, 0.3); }
draw(Lc, 1.1);
show(Llab, 0.4);
}
wait(1.5);
step("trace the right side") {
par { fade(c3, 0.25); show(c4, 0.3); }
draw(Rc, 1.1);
show(Rlab, 0.4);
}
wait(1.5);
step("two crossings") {
par { fade(c4, 0.25); show(c5, 0.3); }
show(P0, 0.4); show(P1, 0.4);
pulse(P0, 0.5); pulse(P1, 0.5);
}
wait(1.6);
step("the rigorous why") {
par { fade(c5, 0.25); show(c6, 0.3); }
}
wait(2.0);
step("the hidden skeleton") {
par { fade(c6, 0.25); show(c7, 0.3); }
draw(Pc, 1.1);
show(Plab, 0.4);
draw(g1, 0.6);
}
wait(1.6);
step("sign follows the parabola") {
par { fade(c7, 0.25); show(c8, 0.3); }
show(dip, 0.2);
to(dip, x, 1, 1.4);
}
wait(1.6);
step("answer") {
par { fade(c8, 0.25); show(c9, 0.3); }
show(ans, 0.6);
show(cta, 0.6);
pulse(ans, 0.6);
}
wait(2.6);
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);
textbook-matrix-inverses
Two exam-style inverse proofs animated through structure rather than row reduction.
The first scales to an integer matrix, proves its columns are orthogonal, and gets
A⁻¹ = Aᵀ from AᵀA = I; the second computes A², observes A³ = I, and
concludes A⁻¹ = A². One persistent proof stage keeps every unchanged expression.
// ============================================================================
// textbook-matrix-inverses.manic
// Two textbook inverse proofs, animated as structural shortcuts:
// (i) orthogonal columns give A^T A = I;
// (ii) a finite-order matrix gives A^3 = I.
// ============================================================================
title("Two Matrix Inverse Proofs — See the Structure");
canvas("9:16");
template("paper");
watermark(manicMark,(220,175),"Made With Manic");
creator(me,"@anish2good name=Manic_Algebra tagline=Textbooks_in_motion yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker,(540,150),"MATRICES · INVERSES · STRUCTURE");
size(kicker,21); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline,(540,218),"Two proofs without row reduction");
size(headline,38); bold(headline); hidden(headline);
text(part,(540,300),"(i) TEST THE COLUMNS");
size(part,23); bold(part); color(part,gold); hidden(part);
rect(stage,(540,805),930,930);
color(stage,panel); outline(stage,dim); opacity(stage,0.72); hidden(stage);
equation(given,(540,500),
`A=\frac{1}{9}\begin{bmatrix}-8&1&4\\4&4&7\\1&-8&4\end{bmatrix}`,
40);
hidden(given);
equation(work,(540,800),`A^{-1}\stackrel{?}{=}A^{\mathsf T}`,48);
hidden(work);
text(reason,(540,1095),"The inverse appears when a product becomes the identity.");
size(reason,24); wrap(reason,820); color(reason,dim); hidden(reason);
line(rule,(170,1235),(910,1235));
color(rule,cyan); stroke(rule,2); dashed(rule,12,9); opacity(rule,0.32);
untraced(rule);
equation(summary,(540,1435),
`\boxed{A^{\mathsf T}A=I\Rightarrow A^{-1}=A^{\mathsf T}}`,
39);
hidden(summary);
text(takeaway,(540,1540),"LOOK FOR A SHORT PRODUCT THAT RETURNS TO I");
size(takeaway,18); bold(takeaway); color(takeaway,gold); hidden(takeaway);
step("read the two inverse claims") {
seq {
par {
show(kicker,0.35); show(headline,0.55); show(part,0.40);
show(stage,0.45); draw(rule,0.75); show(reason,0.40);
}
show(given,0.65);
show(work,0.55);
wait(0.85);
}
}
step("factor out the scale") {
seq {
say(reason,"Write A = M/9. Then ask whether M-transpose times M equals 81I.",0.50,smooth);
rewrite(work,
`M=\begin{bmatrix}-8&1&4\\4&4&7\\1&-8&4\end{bmatrix},\qquad A=\frac{1}{9}M`,
1.05,smooth);
wait(0.70);
}
}
step("recognize orthogonal columns") {
seq {
say(reason,"Every column has squared length 81, and distinct columns have dot product 0.",0.55,smooth);
rewrite(work,
`\begin{aligned}
\|c_1\|^2&=64+16+1=81\\
\|c_2\|^2&=1+16+64=81\\
\|c_3\|^2&=16+49+16=81
\end{aligned}`,
1.10,smooth);
wait(0.55);
rewrite(work,
`\begin{aligned}
c_1^{\mathsf T}c_2&=-8+16-8=0\\
c_1^{\mathsf T}c_3&=-32+28+4=0\\
c_2^{\mathsf T}c_3&=4+28-32=0
\end{aligned}`,
1.10,smooth);
wait(0.70);
}
}
step("the transpose is the inverse") {
seq {
say(reason,"Those six dot products are exactly the entries of M-transpose times M.",0.50,smooth);
rewrite(work,
`M^{\mathsf T}M=\begin{bmatrix}81&0&0\\0&81&0\\0&0&81\end{bmatrix}=81I`,
1.00,smooth);
wait(0.45);
rewrite(work,
`A^{\mathsf T}A=\frac{1}{81}M^{\mathsf T}M=I`,
0.90,smooth);
show(summary,0.55);
par { pulse(work,0.75); pulse(summary,0.75); }
wait(1.15);
}
}
step("begin the second matrix") {
seq {
par {
fade(summary,0.35);
say(part,"(ii) FIND A POWER THAT RETURNS TO I",0.45,smooth);
say(reason,"For the second matrix, compute powers instead of augmenting [A | I].",0.55,smooth);
rewrite(given,
`A=\begin{bmatrix}1&-1&1\\2&-1&0\\1&0&0\end{bmatrix}`,
0.95,smooth);
rewrite(work,`A^{-1}\stackrel{?}{=}A^2`,0.80,smooth);
}
wait(0.85);
}
}
step("square the matrix") {
seq {
say(reason,"First multiply A by itself. Keep this result for one more multiplication.",0.55,smooth);
rewrite(work,
`A^2=\begin{bmatrix}0&0&1\\0&-1&2\\1&-1&1\end{bmatrix}`,
1.00,smooth);
wait(0.80);
}
}
step("the third power is identity") {
seq {
say(reason,"Now A squared times A returns exactly to the identity matrix.",0.50,smooth);
rewrite(work,
`A^3=A^2A=\begin{bmatrix}1&0&0\\0&1&0\\0&0&1\end{bmatrix}=I`,
1.10,smooth);
wait(0.65);
rewrite(work,`AA^2=A^3=I`,0.80,smooth);
rewrite(summary,
`\boxed{AA^2=I\Rightarrow A^{-1}=A^2}`,
0.85,smooth);
show(summary,0.50);
par { pulse(work,0.75); pulse(summary,0.75); }
wait(1.10);
}
}
step("compare the two shortcuts") {
seq {
par {
show(takeaway,0.45);
say(part,"TWO STRUCTURES · ONE DEFINITION",0.45,smooth);
say(reason,"The inverse is whichever matrix multiplies A to give I.",0.50,smooth);
rewrite(given,
`\text{(i) orthogonality}\qquad\text{(ii) finite order}`,
0.90,smooth);
rewrite(work,
`\begin{aligned}
A^{\mathsf T}A=I&\Rightarrow A^{-1}=A^{\mathsf T}\\
AA^2=I&\Rightarrow A^{-1}=A^2
\end{aligned}`,
1.10,smooth);
fade(summary,0.45);
}
par { pulse(work,0.90); pulse(takeaway,0.90); }
wait(2.00);
}
}
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);
boxplot-wave
Reading a box plot, on a labelled chart: eight months of daily-rainfall box plots JUMP up from below into their true medians (building the seasonal wave), a dashed trend line draws through them, then manic signs off.
// boxplot-wave — reading a box plot: the seasonal rainfall wave, built by jumps.
//
// Eight months of daily-rainfall box plots. They start below the frame and JUMP
// up (springy `shift`) into their true medians on the mm axis — staggered, so the
// seasonal wave assembles month by month. A dashed trend line then draws through
// the medians, and the manic wordmark signs off. Educational (labelled axes, a
// real seasonal pattern, how to read a box plot) and lively.
//
// manic examples/boxplot-wave.manic
title("Monthly rainfall through the year");
canvas(1600, 1000);
template("paper");
let ybase = 850; // y of 0 mm
let sc = 9; // pixels per mm
let start = 1060; // off-screen below: the boxes launch from here
// headings
text(hdr, (830, 66), "Monthly rainfall through the year");
size(hdr, 40); color(hdr, #222222);
text(sub, (830, 112), "each box = a month of daily rainfall · line = median, box = middle 50% (IQR), whiskers = range, dot = a storm");
size(sub, 21); color(sub, #7f8aa3);
// ---- y axis (rainfall, mm) ----
line(yax, (200, 300), (200, ybase)); stroke(yax, 2.5); color(yax, #444444);
for k in 0..4 {
let mm = k * 20;
let yv = ybase - mm * sc;
line(ytk{k}, (192, yv), (208, yv)); stroke(ytk{k}, 2); color(ytk{k}, #444444);
counter(ytl{k}, (160, yv), mm, 0); size(ytl{k}, 22); color(ytl{k}, #555555);
}
text(ytit, (205, 270), "mm"); size(ytit, 22); color(ytit, #7f8aa3);
// ---- x axis (month) ----
line(xax, (220, ybase), (1470, ybase)); stroke(xax, 2.5); color(xax, #444444);
text(xtit, (845, 958), "month"); size(xtit, 24); color(xtit, #7f8aa3);
// ---- the eight monthly box plots, created OFF-SCREEN BELOW (they jump up) ----
boxplot(m0, (290, start), "0 1 2 3 4 5 8", 72);
boxplot(m1, (450, start), "1 3 4 5 6 8 11", 72);
boxplot(m2, (610, start), "5 8 10 12 14 18 24", 72);
boxplot(m3, (770, start), "14 20 26 30 34 40 52", 72);
boxplot(m4, (930, start), "18 26 30 35 39 46 60 82", 72);
boxplot(m5, (1090, start), "10 16 20 22 25 30 40", 72);
boxplot(m6, (1250, start), "4 7 9 11 13 17 24", 72);
boxplot(m7, (1410, start), "0 2 3 4 5 7 10", 72);
// month labels
text(x0, (290, 900), "Jan"); text(x1, (450, 900), "Feb"); text(x2, (610, 900), "Apr");
text(x3, (770, 900), "Jun"); text(x4, (930, 900), "Jul"); text(x5, (1090, 900), "Sep");
text(x6, (1250, 900), "Oct"); text(x7, (1410, 900), "Dec");
size(x0, 22); size(x1, 22); size(x2, 22); size(x3, 22); size(x4, 22); size(x5, 22); size(x6, 22); size(x7, 22);
color(x0, #555555); color(x1, #555555); color(x2, #555555); color(x3, #555555);
color(x4, #555555); color(x5, #555555); color(x6, #555555); color(x7, #555555);
// hide each plot's own number labels — the shared axis carries the values now
hidden(m0.iqr); hidden(m0.lmin); hidden(m0.lmed); hidden(m0.lmax); hidden(m0.lq1); hidden(m0.lq3);
hidden(m1.iqr); hidden(m1.lmin); hidden(m1.lmed); hidden(m1.lmax); hidden(m1.lq1); hidden(m1.lq3);
hidden(m2.iqr); hidden(m2.lmin); hidden(m2.lmed); hidden(m2.lmax); hidden(m2.lq1); hidden(m2.lq3);
hidden(m3.iqr); hidden(m3.lmin); hidden(m3.lmed); hidden(m3.lmax); hidden(m3.lq1); hidden(m3.lq3);
hidden(m4.iqr); hidden(m4.lmin); hidden(m4.lmed); hidden(m4.lmax); hidden(m4.lq1); hidden(m4.lq3);
hidden(m5.iqr); hidden(m5.lmin); hidden(m5.lmed); hidden(m5.lmax); hidden(m5.lq1); hidden(m5.lq3);
hidden(m6.iqr); hidden(m6.lmin); hidden(m6.lmed); hidden(m6.lmax); hidden(m6.lq1); hidden(m6.lq3);
hidden(m7.iqr); hidden(m7.lmin); hidden(m7.lmed); hidden(m7.lmax); hidden(m7.lq1); hidden(m7.lq3);
// the trend line through the medians (drawn in after the boxes land)
line(t0, (290, ybase-3*sc), (450, ybase-5*sc)); tag(t0, trend);
line(t1, (450, ybase-5*sc), (610, ybase-12*sc)); tag(t1, trend);
line(t2, (610, ybase-12*sc), (770, ybase-30*sc)); tag(t2, trend);
line(t3, (770, ybase-30*sc), (930, ybase-35*sc)); tag(t3, trend);
line(t4, (930, ybase-35*sc), (1090, ybase-22*sc)); tag(t4, trend);
line(t5, (1090, ybase-22*sc), (1250, ybase-11*sc)); tag(t5, trend);
line(t6, (1250, ybase-11*sc), (1410, ybase-4*sc)); tag(t6, trend);
stroke(trend, 2.5); color(trend, #ff2d95); dashed(trend); untraced(trend);
// the wordmark, waiting for the sign-off
text(word, (1400, 210), "manic"); size(word, 46); color(word, #ff2d95); hidden(word);
// ---- the show ----
// 1) each month JUMPS up into place (springy), building the seasonal wave
stagger(0.26) {
shift(m0, (0, -(start - (ybase - 3 * sc))), 1.3, overshoot);
shift(m1, (0, -(start - (ybase - 5 * sc))), 1.3, overshoot);
shift(m2, (0, -(start - (ybase - 12 * sc))), 1.3, overshoot);
shift(m3, (0, -(start - (ybase - 30 * sc))), 1.3, overshoot);
shift(m4, (0, -(start - (ybase - 35 * sc))), 1.3, overshoot);
shift(m5, (0, -(start - (ybase - 22 * sc))), 1.3, overshoot);
shift(m6, (0, -(start - (ybase - 11 * sc))), 1.3, overshoot);
shift(m7, (0, -(start - (ybase - 4 * sc))), 1.3, overshoot);
}
// 2) draw the seasonal trend, then 3) sign off
draw(trend, 2.5);
show(word, 0.9); pulse(word);
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);
Machine learning
Small, deterministic models whose displayed values are computed rather than staged. The ML examples use progressive focus so forward values, supervised loss, reverse gradients, and parameter updates stay readable on one persistent network.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
manic-ml-scalar-to-tensor
Start with one value, extend it into a vector, arrange values into a matrix, then stack channels into a rank-3 tensor. The only ML noun is tensor; ordinary Manic steps, arrows, reveals, and captions tell the complete dimensional story.
// manic-ml-scalar-to-tensor.manic — ML foundation story
// One value gains an axis, then rows, then stacked channels. The ML-specific
// surface is only tensor(...); ordinary Manic verbs own the explanation.
title("Manic ML — From Scalar to Tensor");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Build_the_dimensions safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
let cell = 38*u;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · DATA FOUNDATIONS");
text(headline, (cx, h*0.105), "One value gains dimensions");
text(caption, (cx, h*0.84), "Begin with one measured value.");
tensor(scalar, (cx, h*0.20), "7", cell, gold);
tensor(vector, (cx, h*0.35), "7 2 -1 4", cell, cyan);
tensor(matrix, (cx, h*0.53), "7 2 -1; 4 0 3; 1 5 6", cell, magenta);
tensor(volume, (cx, h*0.72), "7 2 -1; 4 0 3; 1 5 6 | 2 4 8; 1 3 9; 0 5 6", cell, lime);
text(scalarTitle, (cx, h*0.155), "SCALAR · RANK 0");
text(vectorTitle, (cx, h*0.295), "VECTOR · RANK 1");
text(matrixTitle, (cx, h*0.445), "MATRIX · RANK 2");
text(volumeTitle, (cx, h*0.625), "TENSOR · RANK 3");
arrow(grow1, (cx, h*0.245), (cx, h*0.285));
arrow(grow2, (cx, h*0.395), (cx, h*0.435));
arrow(grow3, (cx, h*0.585), (cx, h*0.615));
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · DATA FOUNDATIONS");
text(headline, (cx, h*0.14), "One value gains dimensions");
text(caption, (cx, h*0.82), "Begin with one measured value.");
tensor(scalar, (w*0.11, h*0.46), "7", cell, gold);
tensor(vector, (w*0.34, h*0.46), "7 2 -1 4", cell, cyan);
tensor(matrix, (w*0.61, h*0.46), "7 2 -1; 4 0 3; 1 5 6", cell, magenta);
tensor(volume, (w*0.86, h*0.46), "7 2 -1; 4 0 3; 1 5 6 | 2 4 8; 1 3 9; 0 5 6", cell, lime);
text(scalarTitle, (w*0.11, h*0.34), "SCALAR · RANK 0");
text(vectorTitle, (w*0.34, h*0.34), "VECTOR · RANK 1");
text(matrixTitle, (w*0.61, h*0.34), "MATRIX · RANK 2");
text(volumeTitle, (w*0.86, h*0.34), "TENSOR · RANK 3");
arrow(grow1, (w*0.17, h*0.46), (w*0.23, h*0.46));
arrow(grow2, (w*0.44, h*0.46), (w*0.51, h*0.46));
arrow(grow3, (w*0.70, h*0.46), (w*0.76, h*0.46));
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · DATA FOUNDATIONS");
text(headline, (cx, h*0.17), "One value gains dimensions");
text(caption, (cx, h*0.84), "Begin with one measured value.");
tensor(scalar, (w*0.24, h*0.34), "7", cell, gold);
tensor(vector, (w*0.73, h*0.34), "7 2 -1 4", cell, cyan);
tensor(matrix, (w*0.24, h*0.65), "7 2 -1; 4 0 3; 1 5 6", cell, magenta);
tensor(volume, (w*0.73, h*0.65), "7 2 -1; 4 0 3; 1 5 6 | 2 4 8; 1 3 9; 0 5 6", cell, lime);
text(scalarTitle, (w*0.24, h*0.235), "SCALAR · RANK 0");
text(vectorTitle, (w*0.73, h*0.235), "VECTOR · RANK 1");
text(matrixTitle, (w*0.24, h*0.505), "MATRIX · RANK 2");
text(volumeTitle, (w*0.73, h*0.505), "TENSOR · RANK 3");
arrow(grow1, (w*0.34, h*0.34), (w*0.50, h*0.34));
arrow(grow2, (w*0.73, h*0.40), (w*0.36, h*0.56));
arrow(grow3, (w*0.35, h*0.65), (w*0.50, h*0.65));
}
size(kicker, 19*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 34*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 21*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
size(scalarTitle, 17*u); bold(scalarTitle); color(scalarTitle, dim); hidden(scalarTitle);
size(vectorTitle, 17*u); bold(vectorTitle); color(vectorTitle, dim); hidden(vectorTitle);
size(matrixTitle, 17*u); bold(matrixTitle); color(matrixTitle, dim); hidden(matrixTitle);
size(volumeTitle, 17*u); bold(volumeTitle); color(volumeTitle, dim); hidden(volumeTitle);
color(grow1, dim); stroke(grow1, 2.5*u); hidden(grow1);
color(grow2, dim); stroke(grow2, 2.5*u); hidden(grow2);
color(grow3, dim); stroke(grow3, 2.5*u); hidden(grow3);
hidden(scalar.cells); hidden(scalar.values); hidden(scalar.labels);
hidden(vector.cells); hidden(vector.values); hidden(vector.labels);
hidden(matrix.cells); hidden(matrix.values); hidden(matrix.labels);
hidden(volume.cells); hidden(volume.values); hidden(volume.labels);
step("scalar") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(scalarTitle, 0.30);
par {
show(scalar.cells, 0.55);
show(scalar.values, 0.55);
}
}
wait(0.55);
step("vector") {
par {
show(grow1, 0.35);
show(vectorTitle, 0.30);
show(vector.cells, 0.60);
show(vector.values, 0.60);
say(caption, "Repeat the value along one ordered axis: position now matters.", 0.40);
}
}
wait(0.60);
step("matrix") {
par {
show(grow2, 0.35);
show(matrixTitle, 0.30);
show(matrix.cells, 0.65);
show(matrix.values, 0.65);
say(caption, "Add rows to the columns: one axis becomes a two-dimensional matrix.", 0.40);
}
}
wait(0.65);
step("tensor") {
par {
show(grow3, 0.35);
show(volumeTitle, 0.30);
show(volume.cells, 0.70);
show(volume.values, 0.70);
say(caption, "Stack matrices as channels: the same values now carry depth and context.", 0.40);
}
}
wait(0.75);
step("one-family") {
pulse(volume.channel1, 0.75);
say(caption, "Scalar, vector, and matrix are all tensors—distinguished by their axes.", 0.45);
}
wait(1.50);
manic-ml-activation-focus
A focused ReLU lesson: draw the truthful activation curve, test one negative and one positive input, then connect the bend to nonlinearity. activation supplies the mathematics while core Manic owns the probes, guides, equation, and pacing.
// manic-ml-activation-focus.manic — one ML noun, one complete lesson
// `activation` supplies the truthful ReLU curve. Core Manic supplies the
// question, equation, input probes, local emphasis, timing, and takeaway.
title("Manic ML — Why ReLU Changes a Neuron");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=See_the_rule safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · ACTIVATION");
text(headline, (cx, h*0.11), "Why does a neuron need ReLU?");
text(caption, (cx, h*0.79), "A neuron first receives an unrestricted number.");
equation(rule, (cx, h*0.60), `\operatorname{ReLU}(x)=\max(0,x)`, 35*u);
activation(reluView, (cx, h*0.38), relu, w*0.74, h*0.22);
line(negativePath, (w*0.13, h*0.466), (cx, h*0.466));
line(positivePath, (cx, h*0.466), (w*0.87, h*0.27));
line(positiveGuide, (w*0.685, h*0.466), (w*0.685, h*0.368));
circle(negativeProbe, (w*0.315, h*0.466), 8*u);
circle(positiveProbe, (w*0.685, h*0.368), 8*u);
text(negativeNote, (w*0.27, h*0.525), "x = −2 → 0");
text(positiveNote, (w*0.73, h*0.525), "x = 2 → 2");
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · ACTIVATION");
text(headline, (cx, h*0.14), "Why does a neuron need ReLU?");
text(caption, (cx, h*0.82), "A neuron first receives an unrestricted number.");
equation(rule, (w*0.79, h*0.36), `\operatorname{ReLU}(x)=\max(0,x)`, 34*u);
activation(reluView, (w*0.38, h*0.49), relu, w*0.52, h*0.48);
line(negativePath, (w*0.12, h*0.677), (w*0.38, h*0.677));
line(positivePath, (w*0.38, h*0.677), (w*0.64, h*0.25));
line(positiveGuide, (w*0.51, h*0.677), (w*0.51, h*0.463));
circle(negativeProbe, (w*0.25, h*0.677), 8*u);
circle(positiveProbe, (w*0.51, h*0.463), 8*u);
text(negativeNote, (w*0.79, h*0.50), "x = −2 → 0");
text(positiveNote, (w*0.79, h*0.59), "x = 2 → 2");
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · ACTIVATION");
text(headline, (cx, h*0.145), "Why does a neuron need ReLU?");
text(caption, (cx, h*0.82), "A neuron first receives an unrestricted number.");
equation(rule, (cx, h*0.67), `\operatorname{ReLU}(x)=\max(0,x)`, 34*u);
activation(reluView, (cx, h*0.40), relu, w*0.72, h*0.28);
line(negativePath, (w*0.14, h*0.509), (cx, h*0.509));
line(positivePath, (cx, h*0.509), (w*0.86, h*0.26));
line(positiveGuide, (w*0.68, h*0.509), (w*0.68, h*0.384));
circle(negativeProbe, (w*0.32, h*0.509), 8*u);
circle(positiveProbe, (w*0.68, h*0.384), 8*u);
text(negativeNote, (w*0.27, h*0.58), "x = −2 → 0");
text(positiveNote, (w*0.73, h*0.58), "x = 2 → 2");
}
size(kicker, 19*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 34*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 21*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
hidden(rule);
color(negativePath, magenta); stroke(negativePath, 5*u); glow(negativePath, 0.35); untraced(negativePath);
color(positivePath, cyan); stroke(positivePath, 5*u); glow(positivePath, 0.35); untraced(positivePath);
color(positiveGuide, dim); stroke(positiveGuide, 2*u); dashed(positiveGuide, 9*u, 7*u); untraced(positiveGuide);
color(negativeProbe, magenta); filled(negativeProbe); glow(negativeProbe, 0.55); hidden(negativeProbe);
color(positiveProbe, cyan); filled(positiveProbe); glow(positiveProbe, 0.55); hidden(positiveProbe);
size(negativeNote, 18*u); color(negativeNote, magenta); bold(negativeNote); hidden(negativeNote);
size(positiveNote, 18*u); color(positiveNote, cyan); bold(positiveNote); hidden(positiveNote);
untraced(reluView.axes);
untraced(reluView.curve);
hidden(reluView.label);
step("question") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
draw(reluView.axes, 0.75);
}
wait(0.55);
step("rule") {
show(rule, 0.55);
say(caption, "ReLU makes one transparent promise: return the larger of zero and x.", 0.40);
}
wait(0.55);
step("negative-input") {
par {
draw(negativePath, 0.75);
show(negativeProbe, 0.35);
show(negativeNote, 0.35);
say(caption, "Negative input is muted at zero; it cannot send negative evidence onward.", 0.40);
}
}
wait(0.65);
step("positive-input") {
par {
draw(positivePath, 0.85);
draw(positiveGuide, 0.55);
show(positiveProbe, 0.35);
show(positiveNote, 0.35);
say(caption, "Positive input passes through unchanged, preserving its strength.", 0.40);
}
}
wait(0.70);
step("activation") {
par {
draw(reluView.curve, 1.10);
show(reluView.label, 0.35);
say(caption, "That small bend gives a network a nonlinear decision boundary.", 0.40);
par { cam((cx, h*0.47), 0.70, smooth); zoom(1.10, 0.70, smooth); }
}
}
wait(0.75);
step("takeaway") {
par {
pulse(reluView.curve, 0.75);
say(caption, "ReLU does not invent a signal: it gates what the neuron already computed.", 0.45);
par { cam((cx, cy), 0.70, smooth); zoom(1.0, 0.70, smooth); }
}
}
wait(1.50);
manic-ml-forward-pass
A creator-first neural-network story: introduce the ReLU activation, reveal a seeded 3→6→4→3 model, then follow one real forward pass into softmax probabilities. network, activation, and forward provide the truthful structure while ordinary named steps, captions, and Creator branding tell the lesson.
// manic-ml-forward-pass.manic — ML1 creator proof
// A real deterministic network computes one prediction. The story keeps the
// model persistent and reveals only the active computation instead of flashing
// every connection at once.
title("Manic ML — A Forward Pass You Can Follow");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=See_the_computation safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
// One story reflows to portrait, feed, square, and landscape. The portrait
// network narrows to reserve a clean probability lane at the right.
if h > 1.45*w {
watermark(manicMark, (w*0.18, h*0.075), "Made With Manic");
text(kicker, (cx, h*0.08), "MANIC ML · FORWARD PASS");
text(headline, (cx, h*0.13), "How does a network choose?");
text(caption, (cx, h*0.72), "First, a neuron keeps positive evidence and removes negative evidence.");
activation(reluView, (cx, h*0.40), relu, w*0.72, h*0.24);
network(model, (cx, h*0.42), "3 6 4 3", "relu tanh softmax", w*0.55, h*0.32, 21);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.18, h*0.10), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · FORWARD PASS");
text(headline, (cx, h*0.14), "How does a network choose?");
text(caption, (cx, h*0.83), "First, a neuron keeps positive evidence and removes negative evidence.");
activation(reluView, (cx, h*0.49), relu, w*0.42, h*0.36);
network(model, (cx, h*0.47), "3 6 4 3", "relu tanh softmax", w*0.64, h*0.39, 21);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · FORWARD PASS");
text(headline, (cx, h*0.13), "How does a network choose?");
text(caption, (cx, h*0.82), "First, a neuron keeps positive evidence and removes negative evidence.");
activation(reluView, (cx, h*0.43), relu, w*0.68, h*0.30);
network(model, (cx, h*0.45), "3 6 4 3", "relu tanh softmax", w*0.64, h*0.40, 21);
}
size(kicker, 20*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 36*u); bold(headline); hidden(headline);
size(caption, 22*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
// A truthful ReLU curve introduces the operation used by the hidden layer.
hidden(reluView);
untraced(reluView.axes);
untraced(reluView.curve);
// 3 inputs → 6 ReLU units → 4 tanh units → 3 softmax probabilities.
// Seed 21 makes the educational model reproducible across every render.
hidden(model);
step("activation") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(reluView, 0.55);
draw(reluView.axes, 0.70);
draw(reluView.curve, 1.20);
}
wait(0.75);
step("meet-the-network") {
fade(reluView, 0.45);
show(model, 0.65);
say(caption, "The same operation now lives inside a small, deterministic network.", 0.45);
}
wait(0.65);
step("forward-pass") {
par {
forward(model, "0.15 0.92 0.38", 4.20, smooth);
say(caption, "Follow the bright path: inputs become evidence, then probabilities.", 0.45);
seq {
par { cam((w*0.24, h*0.47), 0.55, smooth); zoom(1.06, 0.55, smooth); }
wait(0.55);
cam((cx, h*0.47), 0.80, smooth);
wait(0.55);
cam((w*0.76, h*0.47), 0.80, smooth);
wait(0.40);
par { cam((cx, cy), 0.55, smooth); zoom(1.0, 0.55, smooth); }
}
}
}
wait(0.90);
step("takeaway") {
pulse(model.output, 0.75);
say(caption, "The picture is driven by the computed values—not a decorative animation.", 0.45);
}
wait(1.60);
manic-ml-learning-step
One complete supervised learning beat on a persistent network: predict, compare with a target using cross-entropy, send exact reverse-mode gradients backward, update every weight and bias, then restore the exact pre-update checkpoint. loss, backward, checkpoint, update, and restore expose both learning and truthful rollback without pretending that rollback is general machine unlearning.
// manic-ml-learning-step.manic — ML2 creator proof
// One persistent network predicts, measures its mistake, sends exact gradients
// backward, changes its parameters, then rolls that one saved change back.
// No layer is cleared and redrawn; rollback is not claimed as full unlearning.
title("Manic ML — How One Mistake Becomes Learning");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=See_the_learning safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.13, h*0.075), "Made With Manic");
text(kicker, (cx, h*0.08), "MANIC ML · ONE LEARNING STEP");
text(headline, (cx, h*0.13), "How does a model learn?");
text(caption, (cx, h*0.72), "First the network predicts from the current weights.");
network(model, (cx, h*0.42), "3 6 4 3", "relu tanh softmax", w*0.55, h*0.32, 21);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.18, h*0.10), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · ONE LEARNING STEP");
text(headline, (cx, h*0.14), "How does a model learn?");
text(caption, (cx, h*0.83), "First the network predicts from the current weights.");
network(model, (cx, h*0.47), "3 6 4 3", "relu tanh softmax", w*0.64, h*0.39, 21);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · ONE LEARNING STEP");
text(headline, (cx, h*0.13), "How does a model learn?");
text(caption, (cx, h*0.82), "First the network predicts from the current weights.");
network(model, (cx, h*0.45), "3 6 4 3", "relu tanh softmax", w*0.64, h*0.40, 21);
}
size(kicker, 20*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 35*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 22*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
hidden(model);
step("question") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(model, 0.65);
}
wait(0.55);
step("predict") {
par {
forward(model, "0.15 0.92 0.38", 3.20, smooth);
say(caption, "A forward pass turns the input into three probabilities.", 0.40);
}
}
wait(0.55);
step("measure-the-mistake") {
par {
loss(model, "1 0 0", crossentropy, 1.50, smooth);
say(caption, "The correct answer is class 1. Cross-entropy measures the mismatch.", 0.40);
}
}
wait(0.55);
step("send-credit-backward") {
par {
backward(model, 3.20, smooth);
say(caption, "The gradient carries responsibility backward through the same connections.", 0.40);
seq {
par { cam((w*0.76, h*0.47), 0.40, smooth); zoom(1.08, 0.40, smooth); }
wait(0.30);
cam((cx, h*0.47), 0.55, smooth);
wait(0.25);
cam((w*0.24, h*0.47), 0.55, smooth);
wait(0.25);
par { cam((cx, cy), 0.45, smooth); zoom(1.0, 0.45, smooth); }
}
}
}
wait(0.55);
// A zero-duration authored checkpoint captures the exact pre-update weights,
// prediction, target, and loss. It does not add a hidden runtime state.
checkpoint(beforeUpdate, model);
step("learn") {
par {
update(model, 0.18, 2.30, smooth);
say(caption, "Each parameter moves opposite its gradient; the same input is computed again.", 0.40);
}
}
wait(0.70);
step("unlearn") {
par {
restore(model, beforeUpdate, 2.30, smooth);
say(caption, "Exact rollback restores the saved parameters and their earlier prediction.", 0.40);
seq {
par { cam((w*0.24, h*0.47), 0.40, smooth); zoom(1.08, 0.40, smooth); }
wait(0.30);
cam((cx, h*0.47), 0.55, smooth);
wait(0.25);
cam((w*0.76, h*0.47), 0.55, smooth);
wait(0.25);
par { cam((cx, cy), 0.45, smooth); zoom(1.0, 0.45, smooth); }
}
}
}
wait(0.70);
step("takeaway") {
pulse(model.output, 0.75);
say(caption, "This undoes one saved update. Dataset-level unlearning is a different process.", 0.45);
}
wait(1.50);
manic-ml-cnn-edge-story
A tiny image becomes an edge-response feature map and then a pooled summary. The shared scan choreography keeps each receptive field, kernel/operator, arithmetic line, and destination cell synchronized while tensor, kernel, convolve, and pool supply the exact values.
// manic-ml-cnn-edge-story.manic — ML3 creator proof
// A tiny image becomes a feature map and then a pooled summary. Every number is
// computed by Manic; scan coordinates the receptive field and destination.
title("Manic ML — How a CNN Finds an Edge");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=See_the_feature safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
let cell = 48*u;
if h > 1.45*w {
watermark(manicMark, (w*0.13, h*0.075), "Made With Manic");
text(kicker, (cx, h*0.08), "MANIC ML · CONVOLUTION");
text(headline, (cx, h*0.13), "How does a CNN see edges?");
text(caption, (cx, h*0.72), "Start with a tiny image: bright cells form a simple shape.");
tensor(image, (w*0.28, h*0.35), "0 0 0 0 0; 0 0 1 1 0; 0 1 1 1 0; 0 0 1 1 0; 0 0 0 0 0", cell, cyan);
kernel(edge, (w*0.72, h*0.35), "-1 0 1; -2 0 2; -1 0 1", cell, magenta);
convolve(feature, image, edge, (w*0.34, h*0.54), 1, 0, 0, relu, cell);
pool(pooled, feature, (w*0.72, h*0.54), max, 2, 1, 0, cell);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.18, h*0.10), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · CONVOLUTION");
text(headline, (cx, h*0.14), "How does a CNN see edges?");
text(caption, (cx, h*0.82), "Start with a tiny image: bright cells form a simple shape.");
tensor(image, (w*0.17, h*0.47), "0 0 0 0 0; 0 0 1 1 0; 0 1 1 1 0; 0 0 1 1 0; 0 0 0 0 0", cell, cyan);
kernel(edge, (w*0.42, h*0.47), "-1 0 1; -2 0 2; -1 0 1", cell, magenta);
convolve(feature, image, edge, (w*0.68, h*0.47), 1, 0, 0, relu, cell);
pool(pooled, feature, (w*0.89, h*0.47), max, 2, 1, 0, cell);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · CONVOLUTION");
text(headline, (cx, h*0.13), "How does a CNN see edges?");
text(caption, (cx, h*0.82), "Start with a tiny image: bright cells form a simple shape.");
tensor(image, (w*0.18, h*0.44), "0 0 0 0 0; 0 0 1 1 0; 0 1 1 1 0; 0 0 1 1 0; 0 0 0 0 0", cell, cyan);
kernel(edge, (w*0.45, h*0.44), "-1 0 1; -2 0 2; -1 0 1", cell, magenta);
convolve(feature, image, edge, (w*0.70, h*0.44), 1, 0, 0, relu, cell);
pool(pooled, feature, (w*0.89, h*0.44), max, 2, 1, 0, cell);
}
size(kicker, 20*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 35*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 22*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
hidden(image); hidden(edge); hidden(feature); hidden(pooled);
step("pixels") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(image, 0.65);
}
wait(0.55);
step("edge-detector") {
show(edge, 0.55);
say(caption, "This 3×3 kernel compares the left side of each patch with the right.", 0.40);
}
wait(0.55);
step("feature-map") {
show(feature, 0.50);
say(caption, "The feature map begins quiet; each destination waits for one receptive field.", 0.40);
}
wait(0.35);
step("convolution-scan") {
par {
scan(feature, 4.80, smooth);
say(caption, "The same kernel slides, multiplies, sums, applies ReLU, and writes one cell.", 0.40);
seq {
par { cam((w*0.38, h*0.45), 0.75, smooth); zoom(1.10, 0.75, smooth); }
wait(2.95);
par { cam((cx, cy), 0.85, smooth); zoom(1.0, 0.85, smooth); }
}
}
}
wait(0.60);
step("pooling-map") {
show(pooled, 0.50);
say(caption, "Max pooling asks a simpler question: where is the strongest local evidence?", 0.40);
}
wait(0.40);
step("pooling-scan") {
par {
scan(pooled, 3.40, smooth);
say(caption, "Each 2×2 window keeps its first maximum; ties are deterministic.", 0.40);
}
}
wait(0.70);
step("takeaway") {
pulse(pooled.cells, 0.75);
say(caption, "A CNN builds meaning locally: pixels → feature responses → compact evidence.", 0.45);
}
wait(1.50);
manic-ml-token-embedding
A sentence becomes honest word tokens, stable seeded educational lookup vectors, exact sinusoidal positions, and model-input vectors. Repeated words prove that token identity keeps one base embedding while position distinguishes each occurrence.
// manic-ml-token-embedding.manic — ML5 acceptance story
// A repeated word keeps one seeded educational token embedding, while exact
// sinusoidal position makes each occurrence a different model input.
title("Manic ML — From Words to Positioned Embeddings");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Follow_the_representation safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.16, h*0.055), "Made With Manic");
text(kicker, (cx, h*0.055), "MANIC ML · EMBEDDINGS");
text(headline, (cx, h*0.095), "A word needs meaning—and a place");
text(caption, (cx, h*0.84), "Start with text. Keep every boundary and every number inspectable.");
tokenize(words, (cx, h*0.19), "the cat chased the cat", word, w*0.80);
embedding(context, words, (cx, h*0.50), "seeded 6 37", sinusoidal, w*0.90, h*0.48);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.16, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · EMBEDDINGS");
text(headline, (cx, h*0.125), "A word needs meaning—and a place");
text(caption, (cx, h*0.84), "Start with text. Keep every boundary and every number inspectable.");
tokenize(words, (cx, h*0.23), "the cat chased the cat", word, w*0.68);
embedding(context, words, (cx, h*0.54), "seeded 6 37", sinusoidal, w*0.90, h*0.48);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · EMBEDDINGS");
text(headline, (cx, h*0.17), "A word needs meaning—and a place");
text(caption, (cx, h*0.83), "Start with text. Keep every boundary and every number inspectable.");
tokenize(words, (cx, h*0.30), "the cat chased the cat", word, w*0.76);
embedding(context, words, (cx, h*0.60), "seeded 6 37", sinusoidal, w*0.90, h*0.38);
}
size(kicker, 18*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 32*u); bold(headline); wrap(headline, w*0.84); hidden(headline);
size(caption, 20*u); color(caption, dim); wrap(caption, w*0.82); hidden(caption);
hidden(words);
hidden(context);
step("sentence") {
show(kicker, 0.30);
show(headline, 0.42);
show(words.labels, 0.35);
show(words.source, 0.40);
show(caption, 0.35);
}
wait(0.55);
step("tokens") {
par {
show(words.tokens, 0.75);
show(words.indices, 0.75);
say(caption, "Word tokenization turns the sentence into five ordered identities.", 0.38);
}
}
wait(0.60);
step("lookup") {
fade(words, 0.35);
par {
show(context.labels, 0.40);
show(context.tokens, 0.55);
show(context.vectors, 0.95);
say(caption, "A seeded educational lookup gives each token identity one six-number vector.", 0.40);
}
}
wait(0.60);
step("same-word") {
par {
pulse(context.row1, 0.70);
pulse(context.row4, 0.70);
say(caption, "Both copies of cat reuse the same base embedding. The lookup depends on the token—not its location.", 0.42);
}
}
wait(0.65);
step("position") {
par {
show(context.positions, 1.00);
show(context.operators, 0.45);
say(caption, "Sinusoidal position adds a deterministic coordinate for places 0 through 4.", 0.42);
}
}
wait(0.65);
step("model-input") {
par {
show(context.combined, 1.00);
say(caption, "Token vector plus position becomes the model input. The repeated word now carries two different locations.", 0.42);
}
}
wait(0.70);
step("takeaway") {
par {
pulse(context.row1, 0.72);
pulse(context.row4, 0.72);
say(caption, "Meaning says what the token is. Position says where this occurrence belongs.", 0.42);
}
}
wait(1.40);
manic-ml-transformer-attention
One token finds context through a real scaled dot-product self-attention head. Explicit embeddings become Q/K/V, one selected softmax row drives the weighted value mix and residual, and a deterministic educational output projection produces exact top-k probabilities without pretending to be a pretrained language model.
// manic-ml-transformer-attention.manic — ML4 acceptance story
// Explicit token embeddings become Q/K/V, one query row becomes normalized
// attention weights, values mix, a residual is added, and a real output
// projection produces top-k probabilities.
title("Manic ML — One Transformer Attention Head");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Follow_the_attention safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · SELF-ATTENTION");
text(headline, (cx, h*0.105), "One token finds context");
text(caption, (cx, h*0.84), "Start with explicit token embeddings—not decorative wires.");
attention(head, (cx, h*0.38), "Art | ificial | intelligence | transforms | business",
"1 0.2 -0.4 0.7; 0.8 0.1 -0.3 0.6; -0.2 1 0.5 0.3; 0.1 0.6 0.9 -0.2; 0.7 -0.1 0.4 1",
w*0.82, h*0.40, 23);
topk(next, head, 3, (cx, h*0.71), "business | work | world | industry | future | people", 4, w*0.72, h*0.18, 29);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.075), "MANIC ML · SELF-ATTENTION");
text(headline, (cx, h*0.14), "One token finds context");
text(caption, (cx, h*0.83), "Start with explicit token embeddings—not decorative wires.");
attention(head, (w*0.40, h*0.48), "Art | ificial | intelligence | transforms | business",
"1 0.2 -0.4 0.7; 0.8 0.1 -0.3 0.6; -0.2 1 0.5 0.3; 0.1 0.6 0.9 -0.2; 0.7 -0.1 0.4 1",
w*0.68, h*0.58, 23);
topk(next, head, 3, (w*0.84, h*0.52), "business | work | world | industry | future | people", 4, w*0.26, h*0.34, 29);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · SELF-ATTENTION");
text(headline, (cx, h*0.13), "One token finds context");
text(caption, (cx, h*0.84), "Start with explicit token embeddings—not decorative wires.");
attention(head, (cx, h*0.39), "Art | ificial | intelligence | transforms | business",
"1 0.2 -0.4 0.7; 0.8 0.1 -0.3 0.6; -0.2 1 0.5 0.3; 0.1 0.6 0.9 -0.2; 0.7 -0.1 0.4 1",
w*0.84, h*0.42, 23);
topk(next, head, 3, (cx, h*0.72), "business | work | world | industry | future | people", 4, w*0.72, h*0.18, 29);
}
size(kicker, 19*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 34*u); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 21*u); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
hidden(head); hidden(next);
step("tokens") {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.40);
show(head.labels, 0.40);
show(head.tokens, 0.60);
}
wait(0.55);
step("projections") {
par {
show(head.q, 0.55);
show(head.k, 0.55);
show(head.v, 0.55);
say(caption, "Each embedding is projected into a Query, Key, and Value vector.", 0.40);
}
}
wait(0.60);
step("scores") {
show(head.matrix, 0.70);
say(caption, "Scaled dot products become one softmax row: every weight is positive and the row sums to 100%.", 0.45);
}
wait(0.65);
step("attend") {
par {
attend(head, 3, 5.20, smooth);
say(caption, "Focus intelligence: Q asks, K measures relevance, and weighted V carries the context.", 0.45);
seq {
par { cam((cx, h*0.46), 0.75, smooth); zoom(1.07, 0.75, smooth); }
wait(3.45);
par { cam((cx, cy), 0.85, smooth); zoom(1.0, 0.85, smooth); }
}
}
}
wait(0.75);
step("prediction") {
show(next, 0.70);
say(caption, "The residual plus attention mix enters an output projection and a truthful softmax ranking.", 0.45);
}
wait(0.80);
step("takeaway") {
pulse(next.rank0, 0.75);
say(caption, "Attention is selective information flow—not every connection shouting at once.", 0.45);
}
wait(1.50);
manic-ml-transformer-block
One persistent token lane crosses a complete deterministic transformer block: two causal attention heads, concatenation and output projection, both residual/norm stages, a GELU MLP, and exact settled outputs. transformer owns the computation while encode provides a smooth directly seekable explanation.
// manic-ml-transformer-block.manic — ML6 acceptance story
// One persistent token lane passes through multi-head causal attention,
// concatenation, two residual paths, pre-normalization, and a GELU MLP.
title("Manic ML — Inside One Transformer Block");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Follow_the_computation safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · TRANSFORMER BLOCK");
text(headline, (cx, h*0.105), "Inside one transformer block");
text(caption, (cx, h*0.84), "Follow one token lane through every computed stage.");
tokenize(words, (cx, h*0.15), "the cat slept because it dreamed", word, w*0.82);
embedding(context, words, (cx, h*0.41), "seeded 6 37", sinusoidal, w*0.90, h*0.38);
transformer(block, context, (cx, h*0.49), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.56);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · TRANSFORMER BLOCK");
text(headline, (cx, h*0.14), "Inside one transformer block");
text(caption, (cx, h*0.82), "Follow one token lane through every computed stage.");
tokenize(words, (cx, h*0.22), "the cat slept because it dreamed", word, w*0.72);
embedding(context, words, (cx, h*0.54), "seeded 6 37", sinusoidal, w*0.90, h*0.48);
transformer(block, context, (cx, h*0.52), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.92, h*0.62);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.06), "MANIC ML · TRANSFORMER BLOCK");
text(headline, (cx, h*0.13), "Inside one transformer block");
text(caption, (cx, h*0.83), "Follow one token lane through every computed stage.");
tokenize(words, (cx, h*0.23), "the cat slept because it dreamed", word, w*0.78);
embedding(context, words, (cx, h*0.52), "seeded 6 37", sinusoidal, w*0.90, h*0.44);
transformer(block, context, (cx, h*0.52), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.57);
}
size(kicker, 18*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 30*u); bold(headline); wrap(headline, w*0.86); hidden(headline);
size(caption, 19*u); color(caption, dim); wrap(caption, w*0.84); hidden(caption);
hidden(words);
hidden(context);
hidden(block);
step("tokens") {
par {
show(kicker, 0.28);
show(headline, 0.42);
show(words.labels, 0.30);
show(words.source, 0.36);
show(words.tokens, 0.70);
show(words.indices, 0.70);
show(caption, 0.32);
say(caption, "Six ordered tokens begin as one persistent lane.", 0.36);
}
}
wait(0.55);
step("model-input") {
seq {
fade(words, 0.32);
par {
show(context.labels, 0.32);
show(context.tokens, 0.42);
show(context.vectors, 0.82);
say(caption, "Each identity receives a stable lookup vector.", 0.36);
}
par {
show(context.positions, 0.72);
show(context.operators, 0.34);
show(context.combined, 0.78);
say(caption, "Position is added exactly. These six vectors are the block input.", 0.38);
}
}
}
wait(0.60);
step("encode") {
seq {
par {
fade(context, 0.34);
fade(caption, 0.24);
}
par {
encode(block, 6.2, smooth);
seq {
par { cam((cx - w*0.25, h*0.52), 0.75, smooth); zoom(1.08, 0.75, smooth); }
wait(1.35);
cam((cx + w*0.17, h*0.52), 1.15, smooth);
wait(1.50);
par { cam((cx, cy), 0.85, smooth); zoom(1.0, 0.85, smooth); }
}
}
}
}
wait(0.70);
step("takeaway") {
seq {
show(caption, 0.20);
par {
pulse(block.residual1, 0.72);
pulse(block.residual2, 0.72);
pulse(block.output, 0.72);
say(caption, "Attention shares context. The MLP reshapes each token. Residuals keep its identity continuous.", 0.42);
}
}
}
wait(1.35);
manic-ml-logits-sampling
The same educational LM projection is viewed at low and high temperature before greedy and top-p decoding make their support explicit. logits computes every candidate from the final hidden row; sample filters, renormalizes, and selects one reproducible next token without pretending to run a pretrained model.
// manic-ml-logits-sampling.manic — ML7 acceptance story
// The same LM projection is viewed at two temperatures, then top-p sampling
// filters, renormalizes, and makes one reproducible next-token choice.
title("Manic ML — How a Transformer Chooses the Next Token");
canvas("16:9");
template("mono");
creator(me, "@anish2good name=Manic_ML tagline=Models_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Follow_the_computation safe=clean");
socials(me);
let u = (w+h-abs(w-h))/1440;
if h > 1.45*w {
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.07), "MANIC ML · LOGITS → PROBABILITIES → TOKEN");
text(headline, (cx, h*0.115), "How does a transformer choose its next word?");
text(caption, (cx, h*0.83), "One hidden vector. One full distribution. One reproducible choice.");
tokenize(words, (cx, h*0.17), "the model learned to", word, w*0.78);
embedding(context, words, (cx, h*0.42), "seeded 6 37", sinusoidal, w*0.88, h*0.34);
transformer(block, context, (cx, h*0.47), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.50);
logits(cool, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 0.55, w*0.86, h*0.54, 73);
logits(warm, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 1.45, w*0.86, h*0.54, 73);
}
else if w > 1.25*h {
watermark(manicMark, (w*0.17, h*0.09), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · LOGITS → PROBABILITIES → TOKEN");
text(headline, (cx, h*0.13), "How does a transformer choose its next word?");
text(caption, (cx, h*0.82), "One hidden vector. One full distribution. One reproducible choice.");
tokenize(words, (cx, h*0.21), "the model learned to", word, w*0.64);
embedding(context, words, (cx, h*0.50), "seeded 6 37", sinusoidal, w*0.88, h*0.42);
transformer(block, context, (cx, h*0.50), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.58);
logits(cool, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 0.55, w*0.78, h*0.54, 73);
logits(warm, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 1.45, w*0.78, h*0.54, 73);
}
else {
watermark(manicMark, (w*0.18, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.065), "MANIC ML · LOGITS → PROBABILITIES → TOKEN");
text(headline, (cx, h*0.155), "How does a transformer choose its next word?");
text(caption, (cx, h*0.82), "One hidden vector. One full distribution. One reproducible choice.");
tokenize(words, (cx, h*0.21), "the model learned to", word, w*0.70);
embedding(context, words, (cx, h*0.50), "seeded 6 37", sinusoidal, w*0.88, h*0.40);
transformer(block, context, (cx, h*0.50), "heads=2 mask=causal mlp=12 activation=gelu norm=pre dropout=0 mode=inference seed=41", w*0.90, h*0.55);
logits(cool, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 0.55, w*0.82, h*0.54, 73);
logits(warm, block, 4, (cx, h*0.51), "reason | predict | learn | adapt | explain | .", 1.45, w*0.82, h*0.54, 73);
}
size(kicker, 17*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 29*u); bold(headline); wrap(headline, w*0.88); hidden(headline);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86); hidden(caption);
hidden(words);
hidden(context);
hidden(block);
hidden(cool);
hidden(warm);
step("prompt") {
par {
show(kicker, 0.28);
show(headline, 0.42);
show(words.labels, 0.28);
show(words.source, 0.34);
show(words.tokens, 0.62);
show(words.indices, 0.62);
show(caption, 0.28);
say(caption, "The prompt becomes an ordered token lane.", 0.34);
}
}
wait(0.45);
step("hidden-state") {
seq {
fade(words, 0.28);
par {
show(block.output, 0.70);
show(block.labels, 0.26);
say(caption, "The transformer MLP ends at a hidden representation—not probabilities.", 0.40);
}
pulse(block.output, 0.72);
}
}
wait(0.45);
step("cool-temperature") {
seq {
fade(block, 0.30);
sample(cool, "greedy", 3.4, smooth);
say(caption, "Low temperature sharpens every candidate. Greedy keeps only the maximum.", 0.42);
}
}
wait(0.65);
step("warm-temperature") {
seq {
fade(cool, 0.30);
sample(warm, "top-p 0.90 seed=17", 3.8, smooth);
say(caption, "Higher temperature spreads the full softmax. Top-p keeps the smallest 90% nucleus, renormalizes it, then samples.", 0.46);
}
}
wait(0.75);
step("takeaway") {
par {
pulse(warm.temperature, 0.70);
pulse(warm.probabilities, 0.70);
say(caption, "Temperature reshapes probability. Sampling turns that distribution into one reproducible next token.", 0.44);
}
}
wait(1.30);
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).
tides
How the Moon moves every ocean — and why the popular sentence is wrong. “It pulls the water”
predicts ONE bulge and one high tide a day; the sea gives two. Seen from over the north pole:
the Moon laps its orbit, a tide gauge shows two highs a day, then the wrong picture is drawn
properly and demolished. The fix is the whole subject - gravity is a GRADIENT, so subtracting
the pull on Earth’s centre leaves a quadrupole that points outward at BOTH ends. The tidal
field is one formula, (2x,-y), with its radial part projected out (water cannot leave the
surface), and 12,000 parcels of water are advected through it - so the two bulges are
computed by RK4, not drawn. Then the planet turns underneath them while the Moon moves on its
honest 13.2 degrees a day (the equilibrium bulge’s axis is a bound parameter, so the tide
tracks the Moon): the coast comes home, the Moon has not, and the extra 13 degrees of turning
IS the 50 minutes a tide runs late each day. The Sun pulls 179x harder and raises 46% of the
tide because a difference falls off as 1/d^3; add its 12.00-hour clock to the Moon’s 12.4206
and the 14.77-day spring/neap fortnight emerges from two cosines with nothing else typed in.
Ends honestly: the real ocean is not two bulges gliding around a smooth planet - Fundy swings
16 m, much of the Mediterranean nothing. The Moon writes the forcing; the coastline decides.
// tides — how the Moon moves every ocean on Earth, and why the popular sentence is wrong.
//
// "The Moon's gravity pulls on the oceans" predicts ONE bulge and one high tide a day. The
// sea gives two. The fix is the whole subject: what raises a tide is not the pull but the
// DIFFERENCE in pull across the planet — a gradient, and a gradient has two ends.
//
// ACT I the fact: two highs a day, every day
// ACT II the wrong picture, stated properly before it is demolished
// ACT III the gradient — subtract the pull on Earth's centre and a QUADRUPOLE is left.
// The ocean is 12,000 parcels of water `advect`ed through it, so the two bulges
// are computed, not drawn. Then the planet turns under them and one coast passes
// through both: two high tides a day, from one Moon
// ACT IV sideways, not up: 1.1 micrometres per second squared, a ten-millionth of
// gravity. Nothing is lifted; water is herded
// ACT V the Sun pulls 179x harder and loses, because tides go as 1/d^3. Add the two
// and the spring/neap fortnight EMERGES from two cosines
// ACT VI the honest part: the real ocean is not two bulges gliding around
//
// The whole film is one stage — the view down on the north pole, so Earth's rotation is an
// ordinary in-plane `turn` and the water can stay where the Moon put it. The tidal field is
// one formula: with the Moon along +x it is (2x, -y). Water cannot leave the surface, so what
// moves it is the TANGENTIAL part — the radial component projected out, F - (F.r)r — which is
// exactly why a tide is a horizontal shove and not a lift.
//
// manic examples/tides.manic
title("Tides — How the Moon Moves Every Ocean — manic");
canvas("16:9");
template("black");
bloom(0.3, 0.52, 22);
// ---- the mark, up top and above everything, for the whole film ----
text(brand, (640, 32), "maniclang.com");
display(brand); size(brand, 19); color(brand, cyan); opacity(brand, 0.72); z(brand, 100);
// ---- type ----
text(ttl, (640, 92), "How the Moon Moves Every Ocean");
display(ttl); size(ttl, 46); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 146), "and why \"it pulls the water up\" is the wrong answer");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 668), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1060, 624), ""); display(act); size(act, 19); color(act, gold); hidden(act);
// ================================ THE STAGE ================================
// Earth seen from over the north pole, 300 px across, so its surface is at radius 150.
svg(earth, (640, 360), "asset:svg/emoji/1f30e.svg", 300);
circle(orbit, (640, 360), 300); outlined(orbit); outline(orbit, dim);
opacity(orbit, 0.45); hidden(orbit);
svg(moon, (940, 360), "asset:svg/emoji/1f315.svg", 82);
text(moonlab, (940, 470), "the Moon — one lap: 27.3 days");
display(moonlab); size(moonlab, 17); color(moonlab, dim); hidden(moonlab);
// one coast, riding the surface
dot(coast, (640, 210), 9); color(coast, gold); hidden(coast);
text(coastlab, (640, 176), "one coast");
display(coastlab); size(coastlab, 17); color(coastlab, gold); hidden(coastlab);
// a tide gauge: the Moon's own constituent, 12.4206 hours, over two days
coords(gauge, (210, 468), (0, 48), (-1.4, 1.4), 19, 54, 0, 12, 1, "hours", "");
color(gauge, dim); hidden(gauge);
plot(trace, (210, 468), 19, 54, "cos(x/12.4206*tau)", (0, 48));
color(trace, cyan); untraced(trace); hidden(trace);
text(twice, (640, 262), "two highs, every day");
display(twice); size(twice, 21); color(twice, gold); hidden(twice);
// ---- ACT II — the wrong picture ----
for k in 0..7 {
arrow(pull{k}, (556, 240 + k*40), (700, 240 + k*40));
color(pull{k}, gold);
untraced(pull{k});
tag(pull{k}, pulls);
}
// r(t) = R(1 + e·cos t): one bulge facing the Moon — what "it pulls the water" predicts
param(wrong, (640, 360), 150, 150,
"(1 + 0.14*cos(t))*cos(t)", "(1 + 0.14*cos(t))*sin(t)", (0, tau));
color(wrong, gold); untraced(wrong);
text(wronglab, (640, 566), "one bulge ⇒ one high tide a day");
display(wronglab); size(wronglab, 21); color(wronglab, gold); hidden(wronglab);
text(nope, (640, 606), "✗ the sea gives two");
display(nope); size(nope, 21); color(nope, magenta); hidden(nope);
// ---- ACT III — the gradient ----
dot(pnear, (790, 360), 7); color(pnear, gold); hidden(pnear);
dot(pmid, (640, 360), 7); color(pmid, fg); hidden(pmid);
dot(pfar, (490, 360), 7); color(pfar, gold); hidden(pfar);
arrow(gnear, (790, 360), (916, 360)); color(gnear, gold); untraced(gnear);
arrow(gmid, (640, 360), (750, 360)); color(gmid, fg); untraced(gmid);
arrow(gfar, (490, 360), (587, 360)); color(gfar, gold); untraced(gfar);
text(g1, (640, 182), "one pull, unequally felt — gravity falls off as 1/d²");
display(g1); size(g1, 20); color(g1, dim); hidden(g1);
arrow(tnear, (790, 360), (862, 360)); color(tnear, magenta); untraced(tnear);
arrow(tfar, (490, 360), (418, 360)); color(tfar, magenta); untraced(tfar);
text(g2, (640, 182), "subtract the pull on the centre — the whole planet already falls with it");
display(g2); size(g2, 20); color(g2, magenta); hidden(g2);
// THE TIDAL FIELD, tangential part only: F - (F·r̂)r̂ with F = (2x, -y). The epsilon keeps the
// planet's centre finite, where the projection is undefined.
vectorfield(tide, (640, 360), 470, 290,
"0.42*(2*x - x*(2*x*x - y*y)/(x*x + y*y + 0.02))",
"0.42*(-y - y*(2*x*x - y*y)/(x*x + y*y + 0.02))", 15);
color(tide, dim); opacity(tide, 0.55); hidden(tide);
// the ocean: a shell of water on the surface, which the field herds
cloud(sea, 12000, cyan, 0.75) {
let a = (i/12000)*tau;
let w = mod(i, 7) - 3;
let x = 640 + (153 + w*1.7)*cos(a);
let y = 360 + (153 + w*1.7)*sin(a);
let r = 1.5;
let hue = 192 + 10*w;
}
glow(sea, 2); hidden(sea);
// and the shape all that herding is heading for: the equilibrium tide, a prolate ellipsoid
// r(t) = R(1 + e(3cos²t − 1)/2) — with e drawn about 200,000x too big to be visible at all
parameter(phi, (150, 600), 0, 0, 0.4, "the Moon has moved on", 2); hidden(phi.widget);
param(bulge, (640, 360), 150, 150,
"(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*cos(t)",
"(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*sin(t)", (0, tau));
bind(phi, bulge, formula,
"(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*cos(t)",
"(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*sin(t)");
color(bulge, gold); untraced(bulge);
text(twolab, (640, 578), "two bulges ⇒ two high tides a day ✓");
display(twolab); size(twolab, 21); color(twolab, gold); hidden(twolab);
text(spinlab, (640, 606), "");
display(spinlab); size(spinlab, 20); color(spinlab, cyan); hidden(spinlab);
// ---- ACT IV — how gentle it is ----
equation(tiny, (640, 244), `a_{\text{tide}}=\frac{2GMr}{d^3}=1.1\times10^{-6}\ \mathrm{m/s^2}`, 30);
color(tiny, magenta); hidden(tiny);
equation(vsg, (640, 322), `\frac{a_{\text{tide}}}{g}\approx 10^{-7}`, 32);
color(vsg, gold); hidden(vsg);
text(gentle, (640, 400), "it could not lift a puddle — but it can shove an ocean sideways for six hours");
display(gentle); size(gentle, 20); color(gentle, dim); hidden(gentle);
// ---- ACT V — the Sun loses, and the beat ----
equation(sunpull, (640, 240), `\frac{F_{\odot}}{F_{\text{Moon}}} = 179`, 30);
color(sunpull, gold); hidden(sunpull);
equation(suntide, (640, 328), `\frac{a_{\odot}}{a_{\text{Moon}}} = 0.46`, 30);
color(suntide, cyan); hidden(suntide);
text(cube, (640, 404), "a pull goes as 1/d², a difference in pull as 1/d³ — and the Sun is 390× farther");
display(cube); size(cube, 20); color(cube, dim); hidden(cube);
// two constituents added: the Moon's 12.4206 h and the Sun's 12.000 h. Nothing here sets a
// fortnight — the 14.77-day spring/neap envelope is what two cosines DO.
coords(month, (160, 470), (0, 720), (-1.7, 1.7), 1.34, 46, 0, 168, 1, "hours", "");
color(month, dim); hidden(month);
plot(beat, (160, 470), 1.34, 46, "cos(x/12.4206*tau) + 0.46*cos(x/12*tau)", (0, 720));
color(beat, cyan); untraced(beat); hidden(beat);
text(springlab, (635, 330), "spring");
display(springlab); size(springlab, 18); color(springlab, gold); hidden(springlab);
text(neaplab, (397, 330), "neap");
display(neaplab); size(neaplab, 18); color(neaplab, magenta); hidden(neaplab);
text(fortnight, (640, 602), "14.77 days, spring to spring — and nobody typed that number in");
display(fortnight); size(fortnight, 20); color(fortnight, gold); hidden(fortnight);
// ---- ACT VI — the honest part ----
text(truth1, (640, 244), "The real ocean is not two bulges gliding around a smooth planet.");
display(truth1); size(truth1, 24); color(truth1, fg); hidden(truth1);
text(truth2, (640, 312), "Continents are in the way. Basins ring. The tide turns around fixed nodes.");
display(truth2); size(truth2, 21); color(truth2, dim); hidden(truth2);
text(fundy, (400, 400), "Bay of Fundy: 16 m");
display(fundy); size(fundy, 22); color(fundy, gold); hidden(fundy);
text(med, (890, 400), "much of the Mediterranean: ~0");
display(med); size(med, 22); color(med, magenta); hidden(med);
text(truth3, (640, 480), "The Moon writes the forcing. The coastline decides the tide.");
display(truth3); size(truth3, 22); color(truth3, cyan); hidden(truth3);
// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.3);
show(cap, 0.3);
say(cap, "Every coast on Earth does this twice a day, and has done for four billion years.");
par { fade(ttl, 0.8); fade(sub, 0.8); }
show(earth, 0.9);
par { show(orbit, 0.6); show(moon, 0.7); show(moonlab, 0.5); }
wait(0.4);
say(cap, "One Moon, one lap of us every 27.3 days. Everything that follows comes from that.");
// a rigidly turned label arrives upside down, so it steps off for the lap
fade(moonlab, 0.4);
par {
turn(moon, (640, 360), 360, 4.2, smooth);
turn(earth, (640, 360), 90, 4.2, smooth);
}
show(moonlab, 0.4);
wait(0.4);
say(cap, "A tide gauge on any coast, two days of it: high, low, high, low, high.");
// the gauge needs the whole width, so the planet steps out for a moment
par { fade(earth, 0.7); fade(moon, 0.6); fade(moonlab, 0.4); fade(orbit, 0.5); }
// while the stage is dark the Moon takes up its working position, far off to the right,
// where the tidal field's formula puts it
move(moon, (1150, 360), 0.01);
move(moonlab, (1150, 436), 0.01);
say(moonlab, "the Moon");
show(gauge, 0.6);
show(trace, 0.4);
draw(trace, 2.0, smooth);
show(twice, 0.5);
wait(1.4);
say(cap, "Two a day. That number is the whole puzzle — and the usual explanation gets it wrong.");
wait(2.2);
// ================================= ACT II =================================
say(act, "II · the wrong picture");
show(act, 0.4);
par { fade(gauge, 0.6); fade(trace, 0.6); fade(twice, 0.5); }
say(cap, "The Moon's gravity pulls on the oceans. So far, so true.");
par { show(earth, 0.8); show(moon, 0.6); show(moonlab, 0.4); }
wait(0.5);
stagger(0.07) {
for k in 0..7 {
draw(pull{k}, 0.5);
}
}
wait(1.2);
say(cap, "Pull the water toward the Moon and it heaps up on the near side. One heap.");
draw(wrong, 1.2, smooth);
show(wronglab, 0.5);
wait(1.8);
say(cap, "Which is one high tide a day. The sea gives two. Something is missing.");
show(nope, 0.6);
wait(2.2);
// ================================= ACT III =================================
say(act, "III · the difference, not the pull");
par { fade(pulls, 0.6); fade(wrong, 0.6); fade(wronglab, 0.5); fade(nope, 0.5); }
say(cap, "Three places: the near side, the centre, the far side.");
par { show(pnear, 0.4); show(pmid, 0.4); show(pfar, 0.4); }
wait(0.7);
say(cap, "Gravity weakens with distance, so those three pulls are not the same size.");
show(g1, 0.5);
stagger(0.22) {
draw(gnear, 0.6);
draw(gmid, 0.6);
draw(gfar, 0.6);
}
wait(1.8);
say(cap, "But the planet is already falling around its centre. Subtract that pull from all three.");
par { fade(g1, 0.5); fade(gnear, 0.5); fade(gmid, 0.5); fade(gfar, 0.5); }
show(g2, 0.6);
par { draw(tnear, 0.7); draw(tfar, 0.7); }
wait(1.8);
say(cap, "What is left points AWAY at both ends. A gradient has two ends. There is the two.");
wait(2.2);
say(cap, "Do that at every point at once, and this is the field the Moon leaves behind.");
par { fade(g2, 0.5); fade(tnear, 0.5); fade(tfar, 0.5); fade(pnear, 0.4); fade(pmid, 0.4); fade(pfar, 0.4); }
show(tide, 0.9);
wait(1.4);
say(cap, "Now put twelve thousand parcels of water on the surface and let the field push them.");
show(sea, 0.8);
wait(0.5);
advect(sea, tide, 6.5, 0.55);
wait(0.4);
say(cap, "Nothing was placed by hand. The water went where the arrows converge — and there are two.");
draw(bulge, 1.4, smooth);
show(twolab, 0.6);
wait(2.2);
// the payoff: the water stays where the Moon put it, and the planet turns underneath
say(cap, "The bulges belong to the Moon, not to the planet. So turn the planet underneath them.");
// the field's arrows are fixed to the Moon's OLD direction, so they bow out before it moves
par { show(coast, 0.5); show(coastlab, 0.4); fade(sea, 0.8); fade(tide, 0.8); }
wait(0.9);
say(spinlab, "one rotation = one day");
show(spinlab, 0.4);
// one day: Earth turns once, and the Moon does not wait — it moves on 360/27.3 = 13.2 degrees,
// taking the tide's axis with it
par {
fade(coastlab, 0.5);
turn(earth, (640, 360), 360, 7.0, linear);
turn(coast, (640, 360), 360, 7.0, linear);
turn(moon, (640, 360), 13.2, 7.0, linear);
to(phi, value, 0.23, 7.0, linear);
}
say(cap, "One coast, one day, two bulges crossed. Two high tides — and the water never travelled.");
wait(2.2);
// and the reason tide tables slip: the coast is back where it started, the Moon is not
say(cap, "But look: the coast is home and the Moon has moved on thirteen degrees.");
say(spinlab, "the Moon moved on 13° while the planet turned once");
wait(2.0);
say(cap, "So the coast has to chase it — about fifty minutes more of turning, every single day.");
par {
turn(earth, (640, 360), 13.2, 1.6, smooth);
turn(coast, (640, 360), 13.2, 1.6, smooth);
}
say(spinlab, "one tidal day = 24 h 50 min");
wait(2.4);
// ================================= ACT IV =================================
say(act, "IV · sideways, not up");
// the caption turns over with the stage, so no frame is left empty under a stale line
say(cap, "One more correction, and it is the one that surprises people. Look how gentle this is.");
par {
fade(coast, 0.4); fade(spinlab, 0.4); fade(bulge, 0.6); fade(twolab, 0.5);
fade(earth, 0.8); fade(moon, 0.6); fade(moonlab, 0.4);
}
show(tiny, 0.8);
wait(1.6);
show(vsg, 0.7);
say(cap, "A ten-millionth of the gravity holding that ocean down.");
wait(1.8);
show(gentle, 0.6);
say(cap, "Nothing gets lifted. Water gets nudged SIDEWAYS for six hours, and arrives.");
wait(2.4);
// ================================= ACT V =================================
say(act, "V · the Sun loses");
par { fade(tiny, 0.7); fade(vsg, 0.7); fade(gentle, 0.6); }
say(cap, "The Sun pulls Earth a hundred and seventy-nine times harder than the Moon does.");
show(sunpull, 0.8);
wait(1.6);
say(cap, "And raises less than half the tide, because a DIFFERENCE falls off faster than a pull.");
show(suntide, 0.8);
show(cube, 0.6);
wait(2.4);
par { fade(sunpull, 0.6); fade(suntide, 0.6); fade(cube, 0.5); }
say(cap, "So the ocean answers two clocks: 12.42 hours for the Moon, 12.00 for the Sun.");
show(month, 0.6);
show(beat, 0.4);
draw(beat, 3.0, smooth);
wait(0.6);
say(cap, "Add them. Where the two clocks agree the tides run big; where they fight, small.");
par { show(springlab, 0.5); show(neaplab, 0.5); }
wait(1.6);
say(cap, "Spring tides, neap tides — a fortnight apart, out of two cosines and nothing else.");
show(fortnight, 0.7);
wait(2.4);
// ================================= ACT VI =================================
say(act, "VI · the honest part");
par {
fade(month, 0.6); fade(beat, 0.6); fade(springlab, 0.4); fade(neaplab, 0.4);
fade(fortnight, 0.6);
}
show(truth1, 0.8);
say(cap, "Everything so far is the FORCING. It is not the tide you can go and watch.");
wait(1.8);
show(truth2, 0.7);
say(cap, "Water cannot chase the Moon around a planet with two continents in the way.");
wait(1.8);
par { show(fundy, 0.6); show(med, 0.6); }
say(cap, "One bay rings like an organ pipe and swings sixteen metres. Another barely moves.");
wait(2.2);
show(truth3, 0.8);
wait(2.4);
// ================================= ENDCARD =================================
par {
fade(truth1, 0.7); fade(truth2, 0.6); fade(truth3, 0.7);
fade(fundy, 0.5); fade(med, 0.5); fade(cap, 0.6); fade(act, 0.5);
}
text(end1, (640, 318), "The Moon does not lift the sea.");
display(end1); size(end1, 40); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 380), "It pulls one side harder than the other.");
display(end2); size(end2, 30); color(end2, cyan); hidden(end2);
text(end3, (640, 458), "and the ocean charges interest: 3.8 cm of Moon a year, paid out of Earth's spin");
display(end3); size(end3, 19); color(end3, dim); hidden(end3);
text(end4, (640, 522), "— manic");
display(end4); size(end4, 24); color(end4, gold); hidden(end4);
show(end1, 0.9);
show(end2, 0.8);
show(end3, 0.7);
show(end4, 0.6);
wait(2.6);
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
second-law-thermodynamics
Why entropy only grows, in five vignettes: gas particles mixing into far more microstates, heat flowing until two sides equalize, free expansion into new volume, a heat engine that must dump waste heat, and the reversible-vs-irreversible limit.
// ============================================================================
// second-law-thermodynamics.manic — entropy, irreversibility, and time's arrow
// ----------------------------------------------------------------------------
// Engine-test recreation of engine-test-4.mp4. The subject is thermodynamics,
// but the vocabulary stays generic: persistent particles, authored layouts,
// plots, counters, arrows, LaTeX, and ordinary timeline composition.
// ============================================================================
title("Second Law of Thermodynamics");
canvas("9:16");
template("mono");
watermark(manicMark, (150, 145), "Made With Manic");
let cold = 194;
let pink = 326;
let hot = 40;
let blue = 226;
let green = 154;
let red = 10;
// ---------------------------------------------------------------------------
// 1 · MIXING MULTIPLIES THE NUMBER OF POSSIBLE MICROSTATES
// ---------------------------------------------------------------------------
equation(law, (540, 175), `S=k_B\ln\Omega`, 58);
rect(mixBox, (540, 520), 720, 280);
outline(mixBox, dim); outlined(mixBox); stroke(mixBox, 3);
rect(mixLeft, (360, 520), 350, 260); hidden(mixLeft);
rect(mixRight, (720, 520), 350, 260); hidden(mixRight);
line(mixDivider, (540, 390), (540, 650)); color(mixDivider, dim); stroke(mixDivider, 2);
particles(mixCold, mixLeft, 36, 6, 17);
particles(mixWarm, mixRight, 36, 6, 29);
hue(mixCold, cold, 0.72, 0.68); hue(mixWarm, pink, 0.72, 0.68);
glow(mixCold, 0.8); glow(mixWarm, 0.8);
text(mixCaption, (540, 745), "Mixing creates overwhelmingly more possible states");
size(mixCaption, 27); color(mixCaption, dim);
line(mixX, (260, 1040), (570, 1040)); line(mixY, (260, 1040), (260, 840));
color(mixX, dim); color(mixY, dim); stroke(mixX, 2); stroke(mixY, 2);
plot(mixCurve, (260, 1040), 78, 175, "1-exp(-2*x)", (0, 3.6));
hue(mixCurve, blue, 0.55, 0.68); stroke(mixCurve, 4); untraced(mixCurve);
dot(mixMarker, (260, 1040), 7); color(mixMarker, fg); glow(mixMarker, 1.0);
equation(mixDelta, (385, 815), `\Delta S/(Nk_B\ln 2)`, 27); color(mixDelta, dim);
counter(mixCount, (735, 945), 0, 1, "log₁₀(Ω/Ω₀) = ", "");
size(mixCount, 28); hue(mixCount, hot, 0.45, 0.72);
tag(mixBox, s1); tag(mixDivider, s1);
tag(mixCold, s1); tag(mixWarm, s1); tag(mixCaption, s1);
tag(mixX, s1); tag(mixY, s1); tag(mixCurve, s1); tag(mixMarker, s1); tag(mixDelta, s1); tag(mixCount, s1);
// ---------------------------------------------------------------------------
// 2 · HEAT FLOW MAKES TOTAL ENTROPY INCREASE
// ---------------------------------------------------------------------------
rect(hotBox, (315, 500), 260, 240); rect(coldBox, (765, 500), 260, 240);
outline(hotBox, dim); outline(coldBox, dim); outlined(hotBox); outlined(coldBox);
stroke(hotBox, 3); stroke(coldBox, 3);
particles(hotMatter, hotBox, 26, 6, 41); particles(coldMatter, coldBox, 26, 6, 73);
hue(hotMatter, hot, 0.66, 0.68); hue(coldMatter, blue, 0.66, 0.68);
link(heatLink, hotBox, coldBox); hue(heatLink, blue, 0.46, 0.62); stroke(heatLink, 4);
hidden(heatLink);
equation(hotLabel, (315, 325), `T_h`, 36); equation(coldLabel, (765, 325), `T_c`, 36);
hue(hotLabel, hot, 0.58, 0.72); hue(coldLabel, blue, 0.58, 0.72);
counter(hotTemp, (315, 680), 403, 0, "", " K");
counter(coldTemp, (765, 680), 397, 0, "", " K");
hue(hotTemp, hot, 0.55, 0.70); hue(coldTemp, blue, 0.55, 0.70);
text(flowCaption, (540, 780), "Heat flows until both sides reach one temperature");
size(flowCaption, 27); color(flowCaption, dim);
line(flowX, (305, 1130), (650, 1130)); line(flowY, (305, 1130), (305, 880));
color(flowX, dim); color(flowY, dim); stroke(flowX, 2); stroke(flowY, 2);
plot(flowCurve, (305, 1130), 78, 215, "1-exp(-2.4*x)", (0, 3.6));
hue(flowCurve, blue, 0.56, 0.70); stroke(flowCurve, 4); untraced(flowCurve);
dot(flowMarker, (305, 1130), 7); color(flowMarker, fg); glow(flowMarker, 1.0);
equation(flowS, (430, 855), `S_{\mathrm{tot}}`, 30); color(flowS, dim);
tag(hotBox, s2); tag(coldBox, s2); tag(hotMatter, s2); tag(coldMatter, s2);
tag(hotLabel, s2); tag(coldLabel, s2); tag(hotTemp, s2); tag(coldTemp, s2);
tag(flowCaption, s2); tag(flowX, s2); tag(flowY, s2); tag(flowCurve, s2); tag(flowMarker, s2); tag(flowS, s2);
hidden(s2);
// ---------------------------------------------------------------------------
// 3 · FREE EXPANSION: ONE GAS, TWICE THE VOLUME
// ---------------------------------------------------------------------------
rect(expandBox, (540, 520), 720, 280); outline(expandBox, dim); outlined(expandBox); stroke(expandBox, 3);
rect(expandLeft, (360, 520), 350, 260); hidden(expandLeft);
line(expandDivider, (540, 390), (540, 650)); color(expandDivider, dim); stroke(expandDivider, 2);
hidden(expandDivider);
particles(expandGas, expandLeft, 54, 6, 101);
hue(expandGas, cold, 0.38, 0.76); glow(expandGas, 0.7);
text(expandCaption, (540, 745), "More volume means exponentially more possible states");
size(expandCaption, 27); color(expandCaption, dim);
line(expandX, (250, 1040), (560, 1040)); line(expandY, (250, 1040), (250, 845));
color(expandX, dim); color(expandY, dim); stroke(expandX, 2); stroke(expandY, 2);
plot(expandCurve, (250, 1040), 85, 60, "x", (0, 2.6));
hue(expandCurve, green, 0.52, 0.70); stroke(expandCurve, 4); untraced(expandCurve);
dot(expandMarker, (250, 1040), 7); color(expandMarker, fg); glow(expandMarker, 1.0);
equation(expandAxis, (390, 820), `\ln(V/V_0)/\ln 2`, 27); color(expandAxis, dim);
equation(expandStates, (760, 905), `V^N\longrightarrow(2V)^N`, 37);
hue(expandStates, hot, 0.52, 0.70);
counter(expandCount, (760, 1010), 8.9, 1, "log₁₀(Ω/Ω₀) = ", "");
size(expandCount, 26); color(expandCount, dim);
tag(expandBox, s3); tag(expandGas, s3);
tag(expandCaption, s3); tag(expandX, s3); tag(expandY, s3); tag(expandCurve, s3);
tag(expandMarker, s3); tag(expandAxis, s3); tag(expandStates, s3); tag(expandCount, s3);
hidden(s3);
// ---------------------------------------------------------------------------
// 4 · A HEAT ENGINE CANNOT TURN EVERY JOULE INTO WORK
// ---------------------------------------------------------------------------
line(hotRail, (230, 410), (850, 410)); hue(hotRail, hot, 0.72, 0.65); stroke(hotRail, 5);
line(coldRail, (230, 1200), (850, 1200)); hue(coldRail, blue, 0.72, 0.65); stroke(coldRail, 5);
equation(hotRailLabel, (540, 365), `T_h`, 31); equation(coldRailLabel, (540, 1250), `T_c`, 31);
hue(hotRailLabel, hot, 0.56, 0.70); hue(coldRailLabel, blue, 0.56, 0.70);
line(engineX, (265, 1010), (805, 1010)); line(engineY, (265, 1010), (265, 555));
color(engineX, dim); color(engineY, dim); stroke(engineX, 2); stroke(engineY, 2);
equation(engineP, (250, 520), `p`, 28); equation(engineV, (835, 1010), `V`, 28);
color(engineP, dim); color(engineV, dim);
// Two paths share the hot state, separate during the cycle, and reconverge.
// Each visible path is split only to carry the warm/cool colour progression;
// the hidden whole paths give `travel` one continuous geometric route.
plot(engineHotPath, (285, 990), 112, 235, "1/(x+0.65)+0.14+0.05*x*(4.1-x)", (0, 4.1));
plot(engineColdPath, (285, 990), 112, 235, "1/(x+0.65)+0.14-0.035*x*(4.1-x)", (0, 4.1));
hidden(engineHotPath); hidden(engineColdPath);
plot(engineHotA, (285, 990), 112, 235, "1/(x+0.65)+0.14+0.05*x*(4.1-x)", (0, 1.55));
plot(engineHotB, (285, 990), 112, 235, "1/(x+0.65)+0.14+0.05*x*(4.1-x)", (1.55, 4.1));
plot(engineColdA, (285, 990), 112, 235, "1/(x+0.65)+0.14-0.035*x*(4.1-x)", (0, 1.85));
plot(engineColdB, (285, 990), 112, 235, "1/(x+0.65)+0.14-0.035*x*(4.1-x)", (1.85, 4.1));
hue(engineHotA, hot, 0.68, 0.70); hue(engineHotB, red, 0.58, 0.69);
hue(engineColdA, 260, 0.52, 0.72); hue(engineColdB, blue, 0.62, 0.70);
stroke(engineHotA, 5); stroke(engineHotB, 5); stroke(engineColdA, 5); stroke(engineColdB, 5);
untraced(engineHotA); untraced(engineHotB); untraced(engineColdA); untraced(engineColdB);
dot(engineDot, (285, 596), 8); color(engineDot, fg); glow(engineDot, 1.1); hidden(engineDot);
arrow(qHot, (330, 410), (330, 610)); hue(qHot, hot, 0.72, 0.68); stroke(qHot, 4); untraced(qHot);
equation(qHotLabel, (360, 525), `Q_h`, 28); hue(qHotLabel, hot, 0.56, 0.70); hidden(qHotLabel);
arrow(workArrow, (650, 850), (875, 700)); color(workArrow, dim); stroke(workArrow, 4); untraced(workArrow);
equation(workLabel, (880, 660), `W`, 28); color(workLabel, dim); hidden(workLabel);
arrow(qCold, (675, 1000), (675, 1170)); hue(qCold, blue, 0.72, 0.68); stroke(qCold, 4); untraced(qCold);
equation(qColdLabel, (735, 1090), `|Q_c|`, 28); hue(qColdLabel, blue, 0.56, 0.70); hidden(qColdLabel);
text(engineCaption, (540, 1345), "A heat engine must release some heat");
size(engineCaption, 30); hue(engineCaption, hot, 0.38, 0.74);
equation(engineEta, (540, 1490), `\eta_C=1-\frac{T_c}{T_h}<1`, 47);
// Persistent transition strokes preserve object identity between scenes:
// entropy curve → heat connector → expansion divider, then the volume graph
// seeds the heat-engine curve. All are ordinary generic `morph` paths.
plot(mixToHeat, (260, 1040), 78, 175, "1-exp(-2*x)", (0, 3.6));
hue(mixToHeat, blue, 0.55, 0.68); stroke(mixToHeat, 4);
morph(mixToHeat, heatLink); hidden(mixToHeat);
line(heatToExpand, (445, 500), (635, 500));
hue(heatToExpand, blue, 0.46, 0.62); stroke(heatToExpand, 4);
morph(heatToExpand, expandDivider); hidden(heatToExpand);
line(engineSeed, (285, 596), (355, 790)); hidden(engineSeed);
plot(expandToEngine, (250, 1040), 85, 60, "x", (0, 2.6));
hue(expandToEngine, green, 0.52, 0.70); stroke(expandToEngine, 4);
morph(expandToEngine, engineSeed); hidden(expandToEngine);
tag(hotRail, s4); tag(coldRail, s4); tag(hotRailLabel, s4); tag(coldRailLabel, s4);
tag(engineX, s4); tag(engineY, s4); tag(engineP, s4); tag(engineV, s4);
tag(engineHotA, s4); tag(engineHotB, s4); tag(engineColdA, s4); tag(engineColdB, s4);
tag(engineCaption, s4); tag(engineEta, s4);
tag(engineDot, s4future); tag(qHot, s4future); tag(qHotLabel, s4future);
tag(workArrow, s4future); tag(workLabel, s4future);
tag(qCold, s4future); tag(qColdLabel, s4future);
hidden(s4);
// ---------------------------------------------------------------------------
// 5 · SAME ENDPOINTS, DIFFERENT ENTROPY PRODUCTION
// ---------------------------------------------------------------------------
text(revHead, (300, 350), "REV. LIMIT"); text(irrHead, (780, 350), "IRREV");
size(revHead, 28); size(irrHead, 28); hue(revHead, green, 0.45, 0.72); hue(irrHead, red, 0.55, 0.68);
rect(revPanel, (300, 610), 330, 330); rect(irrPanel, (780, 610), 330, 330);
outline(revPanel, dim); outline(irrPanel, dim); outlined(revPanel); outlined(irrPanel);
stroke(revPanel, 3); stroke(irrPanel, 3);
rect(revGasBox, (260, 610), 220, 210); rect(irrGasBox, (740, 610), 220, 210);
hidden(revGasBox); hidden(irrGasBox);
rect(revSmall, (215, 610), 120, 210); rect(irrSmall, (695, 610), 120, 210);
hidden(revSmall); hidden(irrSmall);
particles(revGas, revGasBox, 20, 5, 131, "grid");
particles(irrGas, irrGasBox, 20, 5, 151, "grid");
hue(revGas, green, 0.55, 0.72); hue(irrGas, red, 0.58, 0.68);
line(revPiston, (405, 505), (405, 715)); line(irrPiston, (885, 505), (885, 715));
color(revPiston, dim); color(irrPiston, dim); stroke(revPiston, 5); stroke(irrPiston, 5);
text(compareCaption, (540, 865), "Same endpoints · different entropy production");
size(compareCaption, 28); color(compareCaption, dim);
rect(revMeter, (300, 1120), 70, 260); rect(irrMeter, (780, 1120), 70, 260);
outline(revMeter, dim); outline(irrMeter, dim); outlined(revMeter); outlined(irrMeter);
stroke(revMeter, 3); stroke(irrMeter, 3);
line(irrBar, (780, 1235), (780, 1235)); hue(irrBar, red, 0.72, 0.67); stroke(irrBar, 28);
line(revZero, (278, 1235), (322, 1235)); hue(revZero, green, 0.58, 0.70); stroke(revZero, 5);
equation(revEq, (300, 1325), `S_{\mathrm{gen}}=0`, 31);
equation(irrEq, (780, 1325), `S_{\mathrm{gen}}>0`, 31);
hue(revEq, green, 0.48, 0.70); hue(irrEq, red, 0.55, 0.68);
tag(revHead, s5); tag(irrHead, s5); tag(revPanel, s5); tag(irrPanel, s5);
tag(revGas, s5); tag(irrGas, s5); tag(revPiston, s5); tag(irrPiston, s5); tag(compareCaption, s5);
tag(revMeter, s5); tag(irrMeter, s5); tag(irrBar, s5); tag(revZero, s5); tag(revEq, s5); tag(irrEq, s5);
hidden(s5);
// ---------------------------------------------------------------------------
// 6 · THE ARROW OF TIME IS A STATISTICAL ARROW
// ---------------------------------------------------------------------------
rect(timeBox, (540, 610), 720, 310); outline(timeBox, dim); outlined(timeBox); stroke(timeBox, 3);
particles(timeGas, timeBox, 63, 6, 211, "grid");
hue(timeGas, hot, 0.48, 0.74); glow(timeGas, 0.7);
arrow(timeForward, (300, 980), (780, 980)); hue(timeForward, red, 0.62, 0.69); stroke(timeForward, 5);
equation(timeForwardLabel, (540, 1035), `t`, 28); color(timeForwardLabel, dim);
arrow(timeReverse, (780, 980), (300, 980)); color(timeReverse, fg); stroke(timeReverse, 5); hidden(timeReverse);
equation(timeReverseLabel, (540, 1035), `t`, 28); color(timeReverseLabel, dim); hidden(timeReverseLabel);
text(timeCaption, (540, 1135), "A special state naturally spreads into a typical one");
size(timeCaption, 29); hue(timeCaption, hot, 0.34, 0.74);
equation(returnProb, (540, 285), `\Pr(\mathrm{return})=\frac{\Omega_{\mathrm{low}}}{\Omega_{\mathrm{eq}}}=e^{-\Delta S/k_B}`, 43);
equation(returnOdds, (540, 365), `\Pr(\mathrm{ordered\ return})<10^{-40}`, 34);
hue(returnOdds, hot, 0.48, 0.72); hidden(returnProb); hidden(returnOdds);
tag(timeBox, s6); tag(timeGas, s6gas); tag(timeForward, s6); tag(timeForwardLabel, s6);
tag(timeCaption, s6);
tag(timeReverse, s6future); tag(timeReverseLabel, s6future);
tag(returnProb, s6future); tag(returnOdds, s6future);
hidden(s6); hidden(s6gas);
circle(finalOrbit, (540, 790), 270); hidden(finalOrbit);
for i in 0..72 {
let a = tau*i/72;
dot(finalDot{i}, (540 + 270*cos(a), 790 + 270*sin(a)), 4);
hue(finalDot{i}, 10 + 300*i/72, 0.54, 0.62);
glow(finalDot{i}, 0.5);
tag(finalDot{i}, finalRing);
}
equation(finalLaw, (540, 765), `\Delta S_{\mathrm{universe}}=S_{\mathrm{gen}}\ge 0`, 48);
text(finalCaption, (540, 875), "Irreversible change produces entropy");
size(finalCaption, 31); color(finalCaption, dim);
hidden(finalRing); hidden(finalLaw); hidden(finalCaption);
// ---------------------------------------------------------------------------
// TIMELINE · six continuous beats, approximately 39 seconds
// ---------------------------------------------------------------------------
step("mixing") {
wait(5.5);
par {
seq { wait(0.45); fade(mixDivider, 0.20); }
seq { wait(0.55); arrange(mixCold, mixBox, "random", 0.95, out); wander(mixCold, 4.0); }
seq { wait(0.55); arrange(mixWarm, mixBox, "random", 0.95, out); wander(mixWarm, 4.0); }
seq {
wait(0.55);
par {
draw(mixCurve, 2.20, out);
travel(mixMarker, mixCurve, 2.20, out);
to(mixCount, value, 25.3, 2.20, out);
}
}
}
}
step("heat-flow") {
seq {
par {
fade(s1, 0.65);
show(mixToHeat, 0.10);
to(mixToHeat, morph, 1, 0.65, smooth);
seq { wait(0.20); show(s2, 0.45); }
rewrite(law, `\dot S_{\mathrm{tot}}=K\frac{(T_h-T_c)^2}{T_hT_c}>0`, 0.65, smooth);
}
par {
wander(hotMatter, 4.4);
wander(coldMatter, 4.4);
draw(flowCurve, 3.6, smooth);
travel(flowMarker, flowCurve, 3.6, smooth);
to(hotTemp, value, 400, 4.0, smooth);
to(coldTemp, value, 400, 4.0, smooth);
recolor(hotMatter, cyan, 4.0);
recolor(coldMatter, cyan, 4.0);
seq { flow(mixToHeat, 1.1); flow(mixToHeat, 1.1); flow(mixToHeat, 1.1); flow(mixToHeat, 1.1); }
}
wait(0.45);
}
}
step("free-expansion") {
seq {
par {
fade(s2, 0.65);
fade(mixToHeat, 0.25);
show(heatToExpand, 0.10);
to(heatToExpand, morph, 1, 0.65, smooth);
recolor(heatToExpand, dim, 0.65);
seq { wait(0.20); show(s3, 0.45); }
rewrite(law, `\Delta S=Nk_B\ln 2`, 0.65, smooth);
}
par {
seq { arrange(expandGas, expandBox, "random", 1.10, out); wander(expandGas, 1.45); }
fade(heatToExpand, 0.65);
draw(expandCurve, 2.45, smooth);
travel(expandMarker, expandCurve, 2.45, smooth);
to(expandCount, value, 20.9, 2.45, smooth);
}
wait(0.25);
}
}
step("heat-engine") {
seq {
par {
fade(s3, 0.65);
show(expandToEngine, 0.10);
to(expandToEngine, morph, 1, 0.65, smooth);
seq { wait(0.20); show(s4, 0.45); }
rewrite(law, `\frac{Q_h}{T_h}=\frac{|Q_c|}{T_c}`, 0.65, smooth);
}
par {
seq { draw(engineHotA, 1.85, smooth); draw(engineHotB, 3.35, smooth); }
seq { draw(engineColdA, 2.35, smooth); draw(engineColdB, 2.85, smooth); }
seq {
wait(0.15); show(engineDot, 0.15);
travel(engineDot, engineColdPath, 4.9, smooth);
}
fade(expandToEngine, 0.30);
seq { draw(qHot, 0.7); show(qHotLabel, 0.2); wait(1.0); }
seq { wait(1.3); draw(workArrow, 0.8); show(workLabel, 0.2); }
seq { wait(2.6); draw(qCold, 0.8); show(qColdLabel, 0.2); }
}
wait(2.15);
}
}
step("entropy-generation") {
seq {
par {
fade(s4, 0.50); fade(s4future, 0.50);
show(s5, 0.50);
rewrite(law, `\Delta S_{\mathrm{universe}}=S_{\mathrm{gen}}`, 0.50, smooth);
}
par {
arrange(revGas, revSmall, "grid", 2.4, smooth);
arrange(irrGas, irrSmall, "random", 2.4, smooth);
par { move(revPiston, (275, 505), 2.4, smooth); grow(revPiston, (275, 715), 2.4, smooth); }
par { move(irrPiston, (755, 505), 2.4, smooth); grow(irrPiston, (755, 715), 2.4, smooth); }
grow(irrBar, (780, 1025), 2.4, smooth);
}
wait(1.10);
}
}
step("time-arrow") {
seq {
par {
fade(s5, 0.50); fade(law, 0.50);
show(s6, 0.50); show(s6gas, 0.50);
}
par {
arrange(timeGas, timeBox, "random", 4.0, smooth);
seq { wait(3.0); pulse(timeForward, 0.8); }
}
wait(0.35);
par {
fade(timeForward, 0.30); fade(timeForwardLabel, 0.30);
show(timeReverse, 0.30); show(timeReverseLabel, 0.30);
say(timeCaption, "Exact reversal can reconstruct the past", 0.35);
}
arrange(timeGas, timeBox, "grid", 3.1, smooth);
par {
show(returnProb, 0.45); show(returnOdds, 0.45);
say(timeCaption, "But that reversal must be fantastically precise", 0.40);
}
wait(1.15);
par {
fade(s6, 0.90);
fade(s6future, 0.90);
arrange(timeGas, finalOrbit, "ring", 0.90, smooth);
seq { wait(0.55); fade(s6gas, 0.35); }
seq { wait(0.55); show(finalRing, 0.35); }
seq { wait(0.55); transform(finalRing, (540,790), 0.9511, -0.3090, 0.3090, 0.9511, 0.55, out); }
seq { wait(0.30); par { show(finalLaw, 0.60); show(finalCaption, 0.60); } }
}
wait(1.70);
}
}
maxwell-boltzmann-process
A seeded reservoir releases persistent molecules along one authored path while the same compiled process grows an empirical speed histogram and arrival count. The first acceptance story for evolving collections: motion and measurement stay connected and seekable.
// MAXWELL–BOLTZMANN PROCESS FOUNDATION
// First acceptance story for deterministic evolving collections. The emitted
// particles, arrival count, and speed histogram all come from one compiled
// process. The formula is authored truth; Manic is not pretending to be a CAS.
title("Maxwell–Boltzmann — From Motion to Distribution");
canvas("9:16");
template("blank");
watermark(mark, (155, 92), "Made With Manic");
text(kicker, (540, 80), "STATISTICAL MECHANICS · PROCESS VIEW");
text(headline, (540, 145), "How does molecular motion become a distribution?");
size(kicker, 19); color(kicker, dim); bold(kicker);
size(headline, 33); bold(headline); wrap(headline, 850);
equation(law, (540, 245),
`f(v)=4\pi\left(\frac{m}{2\pi k_BT}\right)^{3/2}v^2e^{-mv^2/(2k_BT)}`, 36);
rect(reservoir, (410, 500), 560, 330);
outlined(reservoir); outline(reservoir, dim); stroke(reservoir, 4);
line(divider, (630, 340), (630, 660));
color(divider, dim); stroke(divider, 3);
text(gasLabel, (330, 685), "seeded molecules");
size(gasLabel, 20); color(gasLabel, dim);
particles(molecules, reservoir, 72, 5, 29);
// The route starts at the aperture and bends into the measurement region.
spline(nozzle, (630, 500), (735, 430), (850, 570), (900, 735));
stroke(nozzle, 4); color(nozzle, fg); untraced(nozzle);
text(nozzleLabel, (850, 395), "effusive path");
size(nozzleLabel, 19); color(nozzleLabel, dim);
livehistogram(speedBins, (540, 1160), 0.35, 0.95, 14, 760, 300, cyan);
text(histTitle, (540, 970), "EMPIRICAL SPEEDS");
size(histTitle, 23); bold(histTitle);
text(axisUnit, (540, 1355), "normalized path lengths per second");
size(axisUnit, 18); color(axisUnit, dim);
counter(arrived, (540, 1460), 0, 0, "detected molecules ", " / 72");
size(arrived, 24); color(arrived, dim);
text(caption, (540, 1545),
"Each bar grows only when a molecule from the same process arrives.");
size(caption, 22); color(caption, dim); wrap(caption, 820);
hidden(molecules); hidden(speedBins); hidden(arrived); hidden(caption);
hidden(nozzleLabel);
step("reservoir") {
par {
show(molecules, 0.55);
show(speedBins, 0.45);
show(arrived, 0.35);
show(caption, 0.35);
}
}
wait(0.45);
step("open-the-aperture") {
par {
draw(nozzle, 0.55);
show(nozzleLabel, 0.35);
}
}
wait(0.25);
step("motion-becomes-data") {
par {
stream(molecules, nozzle, 5.2, 42, smooth);
observe(speedBins, molecules, speed);
observe(arrived, molecules, arrived);
say(caption,
"The motion and the distribution are two views of one deterministic experiment.",
0.45);
}
}
wait(0.7);
step("takeaway") {
par {
pulse(speedBins.bars, 0.8);
say(caption,
"Manic can now connect a changing collection to measurements without frame callbacks.",
0.45);
}
}
wait(1.2);
maxwell-boltzmann-effusion-v2
Expressive Process V2: 180 persistent molecules receive deterministic Maxwell-distributed speeds, traverse sooner when faster, and visibly settle into their measured bins. A bundled native SVG apparatus frames one truthful process, empirical histogram, theory curve, and count.
// EXPRESSIVE PROCESS V2 — MAXWELL–BOLTZMANN EFFUSION
//
// A polished 2.5D acceptance story inspired by engine-test-6.mov. The
// apparatus is a bundled native SVG, while every molecule, detector bin,
// arrival count, and empirical distribution remains a normal Manic entity.
// `emit`, `collect`, and `observe` all read one deterministic process.
title("Maxwell–Boltzmann Effusion — Motion Becomes Evidence");
canvas("9:16");
template("blank");
watermark(mark, (150, 90), "Made With Manic");
text(kicker, (540, 84), "STATISTICAL MECHANICS · EXPRESSIVE PROCESS V2");
text(headline, (540, 142), "A distribution you can watch forming");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
equation(law, (540, 242),
`f(v)=4\pi\left(\frac{m}{2\pi k_BT}\right)^{3/2}v^2e^{-mv^2/(2k_BT)}`, 34);
// The decorative shell is an asset; the changing evidence stays native.
svg(apparatus, (240, 540), "asset:svg/physics/effusion-reservoir.svg", 330);
text(reservoirLabel, (225, 705), "THERMAL RESERVOIR");
size(reservoirLabel, 17); bold(reservoirLabel); color(reservoirLabel, gold);
rect(detectorPanel, (730, 560), 570, 300);
filled(detectorPanel); color(detectorPanel, panel); opacity(detectorPanel, 0.92);
outlined(detectorPanel); outline(detectorPanel, dim); stroke(detectorPanel, 3);
text(detectorTitle, (730, 382), "PARTICLE SPEED v");
size(detectorTitle, 18); bold(detectorTitle); color(detectorTitle, dim);
// The invisible source is precisely at the SVG nozzle.
circle(source, (348, 540), 20);
opacity(source, 0);
particles(molecules, source, 180, 3.2, 73);
color(molecules, magenta); glow(molecules, 0.8); z(molecules, 8);
// Individual lanes diverge around this authored route before being collected.
spline(flight, (350, 540), (415, 500), (465, 515), (500, 545));
hidden(flight);
livehistogram(empirical, (730, 560), 0, 2.2, 14, 540, 240, cyan);
z(empirical, 3); hidden(empirical.count);
// Same horizontal range as the live histogram: 0 .. 4*scale = 2.2.
distribution(theory, (730, 560), "maxwell", 0.55, 0, gold);
hidden(theory.axis); hidden(theory.name);
untraced(theory.curve); z(theory, 7);
text(empiricalKey, (610, 750), "● empirical arrivals");
text(theoryKey, (850, 750), "— theoretical curve");
size(empiricalKey, 17); color(empiricalKey, cyan);
size(theoryKey, 17); color(theoryKey, gold);
counter(arrivals, (540, 850), 0, 0, "detected molecules ", " / 180");
size(arrivals, 24); color(arrivals, dim);
text(caption, (540, 920),
"A thermal source emits many molecular speeds—not one average speed.");
size(caption, 23); wrap(caption, 880); color(caption, dim);
rect(evidencePanel, (540, 1110), 880, 230);
filled(evidencePanel); color(evidencePanel, panel); opacity(evidencePanel, 0.95);
outlined(evidencePanel); outline(evidencePanel, dim); stroke(evidencePanel, 2);
text(evidenceTitle, (540, 1030), "ONE PROCESS · THREE VIEWS");
size(evidenceTitle, 18); bold(evidenceTitle); color(evidenceTitle, dim);
equation(processLaw, (540, 1100),
`\text{motion}\;\longrightarrow\;\text{arrival bin}\;\longrightarrow\;\hat f(v)`, 32);
text(truth, (540, 1190),
"The dots, bars, and count are derived from the same seeded experiment.");
size(truth, 20); color(truth, dim); wrap(truth, 790);
hidden(apparatus); hidden(reservoirLabel); hidden(detectorPanel);
hidden(detectorTitle); hidden(molecules); hidden(empirical);
hidden(empiricalKey); hidden(theoryKey); hidden(arrivals);
hidden(caption); hidden(evidencePanel); hidden(evidenceTitle);
hidden(processLaw); hidden(truth);
step("build-the-apparatus") {
par {
show(apparatus, 0.65);
show(reservoirLabel, 0.45);
show(detectorPanel, 0.55);
show(detectorTitle, 0.35);
show(empirical.axis, 0.55);
show(empirical.bars, 0.55);
show(empirical.min, 0.35);
show(empirical.max, 0.35);
show(empiricalKey, 0.35);
show(theoryKey, 0.35);
show(arrivals, 0.35);
show(caption, 0.40);
}
}
wait(0.45);
step("the-prediction") {
par {
draw(theory.curve, 0.9, smooth);
say(caption,
"Maxwell predicts a characteristic spread with a most probable speed.",
0.40);
par {
cam((cx, cy), 0.75, smooth);
zoom(1.025, 0.75, smooth);
}
}
}
wait(0.35);
step("run-the-experiment") {
par {
emit(molecules, flight, maxwell, 7.0, 48, smooth);
collect(empirical, molecules, speed, 0.48, smooth);
observe(empirical, molecules, speed);
observe(arrivals, molecules, arrived);
show(molecules, 0.15);
say(caption,
"Each molecule keeps its sampled speed. Faster molecules cross the apparatus sooner.",
0.45);
}
}
wait(0.55);
step("motion-becomes-evidence") {
par {
pulse(empirical.bars, 0.8);
show(evidencePanel, 0.40);
show(evidenceTitle, 0.35);
show(processLaw, 0.45);
show(truth, 0.40);
say(caption,
"Collection does not fake the chart: every arrival lands in the bin selected by its speed.",
0.45);
par {
cam((cx, cy), 0.65, smooth);
zoom(1.0, 0.65, smooth);
}
}
}
wait(1.4);
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");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
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);
gradient-fastest-descent
Bernoulli 1696, told THROUGH gradients: four wires race from A to B, each wearing a
3-stop vertical speedometer (v = √(2gΔh) — depth IS speed), then the cycloid’s secret
is revealed by a "curvature" gradient — it bends hardest at the start. A 16:9 3B1B-
style story; companion Short is gradient-fastest-descent-shorts.
// ============================================================================
// gradient-fastest-descent.manic — a 3B1B-style 16:9 story told THROUGH
// gradients (companion: gradient-fastest-descent-shorts.manic)
// ----------------------------------------------------------------------------
// THE PROBLEM (Bernoulli, 1696): a bead slides from A down to B under gravity.
// Which wire shape gets it there fastest? Newton, Leibniz and the Bernoullis
// all answered: the CYCLOID — not the straight line.
//
// THE GRADIENTS DO THE PHYSICS:
// · every ramp wears a 3-stop vertical gradient — for a frictionless bead
// v = √(2gΔh), so DEPTH IS SPEED and the color bar is a real speedometer
// · the winner's secret is revealed by a "curvature" gradient — the cycloid
// bends hardest at the very start, a near-vertical dive that buys speed
// immediately
// · a radial well sinks the stage behind the race
// Every curve, bead and color is computed (RK4 bead-on-wire), not drawn.
// ============================================================================
title("The Fastest Descent");
canvas("16:9");
template("blank");
// ---- backdrop: a radial gravity well behind the race ----
circle(stage, (cx, 380), 300);
filled(stage);
gradient(stage, panel, void, radial);
opacity(stage, 0.4);
// ---- the race: four wires from A to B, beads pre-integrated with RK4 ----
brachistochrone(race, (300, 170), 155);
untraced(race.straight); untraced(race.circle); untraced(race.parabola); untraced(race.cycloid);
hidden(race.bead_straight); hidden(race.bead_circle); hidden(race.bead_parabola); hidden(race.bead_cycloid);
hidden(race.markA); hidden(race.labelA); hidden(race.markB); hidden(race.labelB);
stroke(race.straight, 4); stroke(race.circle, 4); stroke(race.parabola, 4); stroke(race.cycloid, 5);
opacity(race.straight, 0.95); opacity(race.circle, 0.95); opacity(race.parabola, 0.95); opacity(race.cycloid, 1);
// depth IS speed: v = √(2gΔh), so a vertical 3-stop gradient over each wire's
// own bounds is its true speedometer — blue at the top (all potential),
// gold at the bottom (all kinetic). 270 puts the first stop at the bottom.
gradient(race.straight, gold, cyan, blue, 270);
gradient(race.circle, gold, cyan, blue, 270);
gradient(race.parabola, gold, cyan, blue, 270);
gradient(race.cycloid, gold, cyan, blue, 270);
// a second copy of the same sim: only its cycloid is used, re-dressed with a
// CURVATURE gradient for the reveal — same computed curve, different truth
brachistochrone(ghost, (300, 170), 155);
hidden(ghost.straight); hidden(ghost.circle); hidden(ghost.parabola);
hidden(ghost.bead_straight); hidden(ghost.bead_circle); hidden(ghost.bead_parabola); hidden(ghost.bead_cycloid);
hidden(ghost.markA); hidden(ghost.labelA); hidden(ghost.markB); hidden(ghost.labelB);
hidden(ghost.cycloid); untraced(ghost.cycloid);
stroke(ghost.cycloid, 6);
gradient(ghost.cycloid, dim, magenta, "curvature");
glow(ghost.cycloid, 0.6);
// ---- typography ----
caption(q, "shortest path — or fastest?", (cx, 56), 34, gold);
equation(law, (1020, 160), `v=\sqrt{2g\,\Delta h}`, 34);
color(law, cyan); hidden(law);
text(cap, (cx, h - 38), ""); size(cap, 20); color(cap, dim); display(cap);
// ============================== SCRIPT ==============================
// 1 — THE PROBLEM
say(cap, "Bernoulli, 1696 — a bead slides from A down to B under gravity alone", 0.4);
show(race.markA, 0.4); show(race.labelA, 0.3); pulse(race.markA);
show(race.markB, 0.4); show(race.labelB, 0.3); pulse(race.markB);
wait(0.3);
wordpop(q, 0.18);
wait(0.5);
// 2 — THE CANDIDATES, each wearing its speedometer
say(cap, "four wires — colored by depth, and depth IS speed", 0.4);
stagger(0.15) {
draw(race.straight, 0.7);
draw(race.circle, 0.7);
draw(race.parabola, 0.7);
draw(race.cycloid, 0.9);
}
wait(0.3);
show(law, 0.5);
say(cap, "blue = all potential energy · gold = all of it cashed into speed", 0.4);
wait(1.0);
// 3 — THE RACE (real RK4 bead-on-wire integrations)
say(cap, "release all four at once …", 0.4);
par {
show(race.bead_straight, 0.25);
show(race.bead_circle, 0.25);
show(race.bead_parabola, 0.25);
show(race.bead_cycloid, 0.25);
}
run(race, 6);
// 4 — THE WINNER
say(cap, "the CYCLOID wins — the straight line isn't even close", 0.4);
flash(race.cycloid, magenta);
glow(race.bead_cycloid, 1.9);
pulse(race.bead_cycloid);
wait(0.8);
// 5 — THE SECRET, told by a curvature gradient
say(cap, "its secret, colored by curvature: it bends hardest at the very start", 0.4);
par {
fade(race.straight, 0.5);
fade(race.circle, 0.5);
fade(race.parabola, 0.5);
fade(race.cycloid, 0.5);
fade(race.bead_straight, 0.4);
fade(race.bead_circle, 0.4);
fade(race.bead_parabola, 0.4);
fade(race.bead_cycloid, 0.4);
}
show(ghost.cycloid, 0.2);
draw(ghost.cycloid, 1.4);
wait(0.4);
say(cap, "a near-vertical dive buys gold-speed immediately — then spends it on distance", 0.4);
flash(ghost.cycloid, magenta);
wait(1.0);
// 6 — THE POINT
recolor(cap, cyan);
say(cap, "every color here is computed — depth is the speedometer, the bend is the strategy", 0.4);
wait(1.6);
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");
spinning-cylinder-flow
Potential flow around a spinning cylinder — WHY a spun ball curves. Parallel streamlines
(uniform flow) and concentric circles (a free vortex) superpose into the asymmetric flow
around the cylinder: bunched (fast, low-pressure) one side, spread (slow, high-pressure) the
other → a net Magnus force, L=ρUΓ. Every streamline is integrated straight from the
velocity field with trajectory — the asymmetry is computed, not drawn.
// Streamlines of potential flow around a spinning cylinder — the superposition
// that explains the Magnus effect (why a spun ball curves). Uniform flow (parallel
// streamlines) + a free vortex (concentric streamlines) superpose into the
// asymmetric flow around a rotating cylinder: bunched (fast, low-pressure) on one
// side, spread (slow, high-pressure) on the other → a net Magnus force. The combined
// streamlines are integrated straight from the velocity field with `trajectory`.
title("Why a Spinning Ball Curves");
canvas("16:9");
template("neon");
let ox = cx;
let oy = cy + 20;
let sc = 82; // px per field unit
let a = 98; // cylinder radius (px)
// --- stage 1: uniform flow = parallel streamlines ---
for k in 0..11 {
line(uni{k}, (ox-430, oy + (k-5)*44), (ox+430, oy + (k-5)*44));
color(uni{k}, cyan); untraced(uni{k}); tag(uni{k}, uniform);
}
// --- stage 2: a free vortex = concentric circular streamlines ---
for k in 0..5 {
circle(vor{k}, (ox, oy), 55 + k*52); color(vor{k}, magenta); untraced(vor{k}); tag(vor{k}, vortex);
}
// --- stage 3: the superposition — streamlines around the spinning cylinder ---
for i in 0..17 {
let y0 = -4.4 + i*0.55;
trajectory(fl{i},
"1 - 1.44*(x*x-y*y)/((x*x+y*y)^2) - 0.7*y/(x*x+y*y)",
"0 - 2.88*x*y/((x*x+y*y)^2) + 0.7*x/(x*x+y*y)",
(-4.6, y0), (ox, oy), sc, 520);
color(fl{i}, cyan); glow(fl{i}, 0.4); untraced(fl{i}); tag(fl{i}, combo);
}
circle(cyl, (ox, oy), a); color(cyl, gold); filled(cyl); hidden(cyl);
circle(cylo, (ox, oy), a); color(cylo, gold); outline(cylo, gold); hidden(cylo);
// spin arrow (clockwise) + labels + net force + Kutta–Joukowski
arrow(spinT, (ox-34, oy-a-8), (ox+34, oy-a-8)); color(spinT, gold); hidden(spinT);
arrow(spinB, (ox+34, oy+a+8), (ox-34, oy+a+8)); color(spinB, gold); hidden(spinB);
arrow(inflow, (ox-430, oy+230), (ox-330, oy+230)); color(inflow, dim); hidden(inflow);
text(inl, (ox-360, oy+205), "U"); size(inl, 22); color(inl, dim); hidden(inl);
arrow(force, (ox+a+70, oy+30), (ox+a+70, oy-90)); color(force, lime); stroke(force, 5); hidden(force);
text(fl_lab, (ox+a+150, oy-30), "Magnus\nforce"); size(fl_lab, 22); color(fl_lab, lime); hidden(fl_lab);
text(fast, (ox, oy-a-70), "streamlines bunch → faster → LOW pressure"); size(fast, 20); color(fast, cyan); wrap(fast, 520); hidden(fast);
text(slow, (ox, oy+a+70), "spread → slower → HIGH pressure"); size(slow, 20); color(slow, magenta); wrap(slow, 520); hidden(slow);
equation(kj, (cx, h-70), `L=\rho\,U\,\Gamma`, 40); color(kj, lime); hidden(kj);
text(cap, (cx, h-30), ""); size(cap, 22); color(cap, dim); wrap(cap, w*0.8); hidden(cap);
show(cap, 0.3);
// 1 — uniform flow
say(cap, "uniform flow — parallel streamlines");
stagger(0.05) { for k in 0..11 { draw(uni{k}, 0.6); } }
wait(0.6);
// 2 — a free vortex
say(cap, "a free vortex — concentric streamlines (the spin's circulation)");
stagger(0.08) { for k in 0..5 { draw(vor{k}, 0.7); } }
wait(0.7);
// 3 — superpose them
say(cap, "superpose them → the flow around a spinning cylinder");
par {
fade(uniform, 0.5); fade(vortex, 0.5);
show(cyl, 0.4); show(cylo, 0.4);
}
stagger(0.04) { for i in 0..17 { draw(fl{i}, 0.9); } }
wait(0.6);
// 4 — the asymmetry → pressure → force
say(cap, "the flow is faster on top, slower below — Bernoulli does the rest");
par {
show(spinT, 0.4); show(spinB, 0.4);
show(inflow, 0.4); show(inl, 0.4);
show(fast, 0.5); show(slow, 0.5);
}
wait(0.5);
par { show(force, 0.6); show(fl_lab, 0.5); show(kj, 0.5); }
wait(0.6);
// 5 — the payoff
say(cap, "a pressure difference pushes it sideways — the Magnus force that curls a free kick");
pulse(force, 0.8);
wait(1.6);
Chemistry — real molecules
molecule3 reads an MDL structure file — a PubChem Download → SDF, or a .mol from RDKit — and lays out its atoms and bonds as ordinary 3-D entities. Nothing about the shape is authored: the coordinates are the file’s, in Ångströms, so a bond angle on screen is the one in the conformer someone computed. Atoms are CPK/Jmol coloured and sized by covalent radius; the molecule turns about its own axis by default, because a shape only reads in three dimensions. structure is the flat half — the skeletal formula a chemist puts on paper, with carbons as bare vertices, OH and NH2 folded from the file’s explicit hydrogens, double bonds leaning into the ring, and wedge/hash bonds where the file records stereochemistry. See the chemistry guide.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
chemistry-kit-promo
Five things a structure file can teach, in one scene and with no coordinate in the script: four molecules with four geometries (bent, pyramidal, tetrahedral, linear); the bond-order ladder, where a C-C, a C=C and a C≡C visibly tighten left to right because the files simply disagree about how far apart two carbons sit; salt against water, where the ionic pair has NO bond in its file and so no stick on the screen; caffeine in all four viewer styles; and cholesterol beside paclitaxel at 74 and 113 atoms.
// Molecules, from files that know their own shape
//
// Six things you can teach from a structure file, and not a coordinate in this script. Every atom
// position comes from a PubChem download, so each claim below is a measurement rather than a
// drawing: shape, bond order, isotope, ionic vs covalent, conformation, and scale.
//
// The molecules turn because that is the default. A shape only reads in three dimensions — flat,
// methane is four lines from a dot.
title("real molecules, real geometry");
canvas("16:9");
template("black");
text(brand, (640, 42), "maniclang.com");
display(brand);
size(brand, 20);
color(brand, dim);
camera3((0, -15, 8.5), (0, 0, 0), 40);
// ── 1. shape: four molecules, four geometries ──
molecule3(waterm, "asset:molecules/water.sdf", (-6.4, 0, 0), 1.5);
molecule3(ammonia, "asset:molecules/ammonia.sdf", (-2.1, 0, 0), 1.5);
molecule3(methane, "asset:molecules/methane.sdf", (2.2, 0, 0), 1.5);
molecule3(co2, "asset:molecules/carbon-dioxide.sdf", (6.6, 0, 0), 1.5);
hidden(waterm); hidden(ammonia); hidden(methane); hidden(co2);
text(h1, (640, 120), "the same four atoms count — four different shapes");
size(h1, 27); color(h1, cyan); hidden(h1);
text(n1, (196, 566), "H2O"); text(v1, (196, 604), "bent");
text(n2, (452, 566), "NH3"); text(v2, (452, 604), "pyramidal");
text(n3, (760, 566), "CH4"); text(v3, (760, 604), "tetrahedral");
text(n4, (1060, 566), "CO2"); text(v4, (1060, 604), "linear");
for i in 1..5 {
size(n{i}, 29); size(v{i}, 21); color(v{i}, dim);
hidden(n{i}); hidden(v{i});
}
color(n1, red); color(n2, blue); color(n3, silver); color(n4, red);
wait(0.5);
show(h1, 0.5);
par { show(waterm, 0.6); show(n1, 0.4); show(v1, 0.4); }
wait(0.5);
par { show(ammonia, 0.6); show(n2, 0.4); show(v2, 0.4); }
wait(0.5);
par { show(methane, 0.6); show(n3, 0.4); show(v3, 0.4); }
wait(0.5);
par { show(co2, 0.6); show(n4, 0.4); show(v4, 0.4); }
wait(2.0);
// ── 2. bond order: a stronger bond is a shorter one ──
//
// Three carbon–carbon bonds, side by side and at one magnification. Nothing scales them to make
// the point; the files simply disagree about how far apart two carbons sit.
par {
fade(waterm, 0.4); fade(ammonia, 0.4); fade(methane, 0.4); fade(co2, 0.4); fade(h1, 0.4);
fade(n1, 0.3); fade(v1, 0.3); fade(n2, 0.3); fade(v2, 0.3);
fade(n3, 0.3); fade(v3, 0.3); fade(n4, 0.3); fade(v4, 0.3);
}
// Hydrogens off and spin off, so each molecule is reduced to the ONE bond being compared and
// holds still long enough to compare it. Ethane comes from record 2 of the bundled alkane file —
// an SDF is a library as often as it is a molecule.
molecule3(ethane, "asset:molecules/alkanes.sdf", (-5.8, 0, 0), 2.6, "hydrogens=0 spin=0 record=2");
molecule3(ethene, "asset:molecules/ethene.sdf", (0, 0, 0), 2.6, "hydrogens=0 spin=0");
molecule3(ethyne, "asset:molecules/ethyne.sdf", (5.8, 0, 0), 2.6, "hydrogens=0 spin=0");
hidden(ethane); hidden(ethene); hidden(ethyne);
text(h2, (640, 120), "one bond, two bonds, three — and it pulls tighter each time");
size(h2, 27); color(h2, lime); hidden(h2);
text(b1, (300, 580), "C-C single");
text(b2, (640, 580), "C=C double");
text(b3, (980, 580), "C≡C triple");
for i in 1..4 { size(b{i}, 22); color(b{i}, dim); hidden(b{i}); }
show(h2, 0.5);
par { show(ethane, 0.6); show(b1, 0.4); }
wait(0.5);
par { show(ethene, 0.6); show(b2, 0.4); }
wait(0.5);
par { show(ethyne, 0.6); show(b3, 0.4); }
wait(2.2);
// ── 3. ionic against covalent ──
//
// Salt has no bonds in its file at all — two ions, drawn apart because nothing joins them. Water
// does. That distinction is in the data, not in the drawing.
par {
fade(ethane, 0.4); fade(ethene, 0.4); fade(ethyne, 0.4); fade(h2, 0.4);
fade(b1, 0.3); fade(b2, 0.3); fade(b3, 0.3);
}
molecule3(salt, "asset:molecules/sodium-chloride.sdf", (-4.4, 0, 0), 1.9);
molecule3(wat2, "asset:molecules/water.sdf", (4.4, 0, 0), 1.9);
hidden(salt); hidden(wat2);
text(h3, (640, 120), "no bond in the file, no stick on the screen");
size(h3, 27); color(h3, amber); hidden(h3);
text(c1, (330, 580), "NaCl - two ions, nothing between them");
text(c2, (980, 580), "H2O - bonded");
for i in 1..3 { size(c{i}, 21); color(c{i}, dim); hidden(c{i}); }
show(h3, 0.5);
par { show(salt, 0.6); show(c1, 0.4); }
wait(0.6);
par { show(wat2, 0.6); show(c2, 0.4); }
wait(2.2);
// ── 4. what a molecule really looks like ──
//
// Caffeine four ways. Ball-and-stick shows what is bonded to what; hiding the hydrogens leaves the
// skeleton a chemist draws; space-filling shows the shape the molecule presents to the world,
// which is the one that decides what it fits into.
par {
fade(salt, 0.4); fade(wat2, 0.4); fade(h3, 0.4); fade(c1, 0.3); fade(c2, 0.3);
}
molecule3(k1, "asset:molecules/caffeine.sdf", (-8.4, 0, 0), 0.52, "style=ball");
molecule3(k2, "asset:molecules/caffeine.sdf", (-2.8, 0, 0), 0.52, "style=sticks");
molecule3(k3, "asset:molecules/caffeine.sdf", (2.8, 0, 0), 0.52, "style=sticks hydrogens=0");
molecule3(k4, "asset:molecules/caffeine.sdf", (8.4, 0, 0), 0.52, "style=spacefill");
hidden(k1); hidden(k2); hidden(k3); hidden(k4);
text(h4, (640, 120), "caffeine - one file, four questions");
size(h4, 27); color(h4, magenta); hidden(h4);
text(s1, (222, 596), "ball & stick");
text(s2, (492, 596), "sticks");
text(s3, (770, 596), "no hydrogens");
text(s4, (1052, 596), "space-filling");
for i in 1..5 { size(s{i}, 20); color(s{i}, dim); hidden(s{i}); }
show(h4, 0.5);
par { show(k1, 0.5); show(s1, 0.4); }
wait(0.45);
par { show(k2, 0.5); show(s2, 0.4); }
wait(0.45);
par { show(k3, 0.5); show(s3, 0.4); }
wait(0.45);
par { show(k4, 0.5); show(s4, 0.4); }
wait(2.4);
// ── 5. and the scale it goes to ──
//
// Cholesterol is 74 atoms; paclitaxel is 113. Same builtin, same three lines.
par {
fade(k1, 0.4); fade(k2, 0.4); fade(k3, 0.4); fade(k4, 0.4); fade(h4, 0.4);
fade(s1, 0.3); fade(s2, 0.3); fade(s3, 0.3); fade(s4, 0.3);
}
molecule3(chol, "asset:molecules/cholesterol.sdf", (-5.2, 0, 0), 0.62, "hydrogens=0");
molecule3(taxol, "asset:molecules/paclitaxel.sdf", (5.0, 0, 0), 0.46, "hydrogens=0");
hidden(chol); hidden(taxol);
text(h5, (640, 120), "and it does not care how big the molecule gets");
size(h5, 27); color(h5, cyan); hidden(h5);
text(g1, (330, 596), "cholesterol - 74 atoms");
text(g2, (980, 596), "paclitaxel - 113 atoms");
for i in 1..3 { size(g{i}, 21); color(g{i}, dim); hidden(g{i}); }
show(h5, 0.5);
par { show(chol, 0.6); show(g1, 0.4); }
wait(0.7);
par { show(taxol, 0.6); show(g2, 0.4); }
wait(2.6);
// ── the close ──
par {
fade(chol, 0.5); fade(taxol, 0.5); fade(h5, 0.4); fade(g1, 0.3); fade(g2, 0.3);
}
molecule3(last, "asset:molecules/caffeine.sdf", (0, 0, 0), 1.15, "spin=18");
hidden(last);
text(end1, (640, 168), "three lines of Manic");
size(end1, 34); color(end1, silver); hidden(end1);
text(end2, (640, 578), "molecule3(caf, \"asset:molecules/caffeine.sdf\");");
size(end2, 23); color(end2, lime); hidden(end2);
text(end3, (640, 616), "every angle measured, not drawn");
size(end3, 21); color(end3, dim); hidden(end3);
par { show(last, 0.8); show(end1, 0.6); }
wait(0.6);
par { show(end2, 0.5); show(end3, 0.5); }
wait(3.2);
chemistry-cinema
The whole kit, in one run, explained by motion. Ten ideas in the order they build on each other, each with a chapter title naming what is on screen — and that is the only prose in it. The rest of what is written is what chemistry writes down anyway: an element symbol, a wavelength, a coefficient, a voltage, a wavenumber. It opens already drawn, on caffeine from its real 2-D record, so the first frame is one worth seeing before anything is pressed. Then an SN2 with curly arrows anchored to actual atoms; an electron falling between computed levels and the colour that falls out of λ = hc/ΔE; water’s three modes and carbon dioxide’s silent one; 72 hard discs reacting only when a collision clears the barrier; coefficients landing one at a time; a crystal coming apart at its corners into hydration shells that face the right way; electrons sent round a wire at a computed current; a bond turning against the energy it costs — and back to the molecule it opened on. Nothing in it is only a picture.
// Chemistry, without a word of it
//
// Eleven ideas in one run, in the order they build on each other. Each act carries a chapter title
// saying what is on screen, and that is the only prose in the film: everything else written down is
// what chemistry writes down anyway — an element symbol, a wavelength, a coefficient, a voltage, a
// wavenumber. The explaining is done by motion, which is the point of the whole kit, since every one
// of these is a thing a still picture cannot say.
//
// 0 A MOLECULE caffeine, from its real record, standing there before anything moves
// 1 LEWIS the electron bookkeeping of NO₃⁻, worked — and a double bond that will not settle
// 2 MECHANISM SN2 — curly arrows aimed at actual atoms, one bond made and one broken
// 3 LIGHT an electron falls between computed levels; the photon's colour comes from λ = hc/ΔE
// 4 MATTER a molecule's real geometry, and the modes it vibrates in
// 5 SILENCE the same for CO₂ — one mode moves no dipole, and its peak is simply absent
// 6 COLLISION a gas reacts only when a collision clears the barrier; the tail does the work
// 7 BALANCE the coefficients that conserve every atom, landing one at a time
// 8 SOLUTION a lattice comes apart at its corners, into hydration shells that face the right way
// 9 CURRENT the same electrons, made to go round a wire instead
// 10 ROTATION a bond turns, and the energy it costs is a curve it rides
//
// It opens on a drawn molecule rather than on black: the first frame is a frame someone might see
// before they press anything.
//
// Nothing here is drawn by hand. The skeletal formula is a real 2-D record, the curly arrows are
// anchored to the atoms and bonds they point at, the levels are −13.606/n², the modes are eigenvectors of a
// mass-weighted Hessian, the reaction is 72 hard discs meeting above an activation energy, the
// coefficients are the null space of the atom matrix, the Lewis structure is counted out from the
// formula, the dissolution order is coordination number,
// the cell's polarity and voltage are its own electrode potentials, and the torsion profile is a
// rigid scan of butane's real geometry. Change any number in the source and the film changes with
// it, because there is nothing in it that is only a picture.
title("chemistry, without a word of it");
canvas("16:9");
template("black");
bloom(0.38, 0.72, 16);
text(brand, (640, 26), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
// ── the chapter titles: each one names what is on screen, and they are the only prose here ──
text(t0, (640, 82), "01 · a molecule");
size(t0, 21);
color(t0, fg);
text(tl, (640, 82), "02 · counting the electrons");
size(tl, 21);
color(tl, fg);
hidden(tl);
text(t1, (640, 82), "03 · substitution, SN2");
size(t1, 21);
color(t1, fg);
hidden(t1);
text(t2, (640, 82), "04 · levels, and the colour of light");
size(t2, 21);
color(t2, fg);
hidden(t2);
text(t3, (640, 82), "05 · how a molecule vibrates");
size(t3, 21);
color(t3, fg);
hidden(t3);
text(t4, (640, 82), "06 · the mode with no peak");
size(t4, 21);
color(t4, fg);
hidden(t4);
text(t5, (640, 82), "07 · collision, and activation energy");
size(t5, 21);
color(t5, fg);
hidden(t5);
text(t6, (640, 82), "08 · balancing an equation");
size(t6, 21);
color(t6, fg);
hidden(t6);
text(t7, (640, 82), "09 · why a salt dissolves");
size(t7, 21);
color(t7, fg);
hidden(t7);
text(t8, (640, 82), "10 · a galvanic cell");
size(t8, 21);
color(t8, fg);
hidden(t8);
text(t9, (640, 82), "11 · turning a single bond");
size(t9, 21);
color(t9, fg);
hidden(t9);
// ── 0 · A MOLECULE — on screen from the first frame ─────────────────────────
structure(caf, "asset:molecules/caffeine-2d.sdf", (640, 368), 116);
color(caf.bonds, fg);
// ── 1 · LEWIS — the electron bookkeeping, worked from the formula ───────────
lewis(lw, "NO3-", (640, 350), 150, 34);
hidden(lw);
// ── 2 · MECHANISM ────────────────────────────────────────────────────────────
//
// Bromoethane and hydroxide. The nucleophile sits below and left of the carbon while the bromine is
// above and right of it — backside attack, which is geometry rather than layout.
structure(sub, "CCBr", (400, 300), 88);
structure(nuc, "[OH-]", (214, 452), 88);
arrow(att, nuc.a0, sub.a1, 62);
color(att, cyan);
stroke(att, 3);
untraced(att);
arrow(go, sub.b1, sub.a2, 40);
color(go, coral);
stroke(go, 3);
untraced(go);
arrow(rxn, (620, 340), (752, 340));
color(rxn, fg);
stroke(rxn, 3);
untraced(rxn);
structure(pro, "CCO", (912, 300), 88);
structure(lea, "[Br-]", (1128, 452), 88);
hidden(sub);
hidden(nuc);
hidden(pro);
hidden(lea);
// ── 2 · LIGHT ────────────────────────────────────────────────────────────────
levels(lv, (330, 350), 250, 320, 6);
emission(spec, lv, (930, 330), 520, 104);
hidden(lv);
hidden(spec);
// ── 3 · MATTER ───────────────────────────────────────────────────────────────
vibration(h2o, "asset:molecules/water.sdf", (390, 350), 150, 22);
irspectrum(irw, h2o, (950, 340), 470, 150, 15);
hidden(h2o);
hidden(irw);
hidden(h2o.readout);
// ── 4 · SILENCE ──────────────────────────────────────────────────────────────
vibration(co2, "asset:molecules/carbon-dioxide.sdf", (390, 350), 150, 22);
irspectrum(irc, co2, (950, 340), 470, 150, 15);
hidden(co2);
hidden(irc);
hidden(co2.readout);
// ── 5 · COLLISION ────────────────────────────────────────────────────────────
gas(gs, (400, 372), 430, 330, 72, "temperature=1.6 radius=7 steps=400 seed=5");
species(gs, A, 0.5, cyan);
species(gs, B, 0.5, magenta);
species(gs, C, gold);
rule(gs, "A + B -> C + C when energy > 3.2");
speeds(gs, (960, 268), 440, 130, 12, 13);
timegraph(gs, (960, 520), 96);
hidden(gs.box);
hidden(gs.particles);
hidden(gs.speeds.axis);
hidden(gs.speeds.bars);
untraced(gs.speeds.mb);
hidden(gs.time.frame);
hidden(gs.time.title);
hidden(gs.time.sweep);
untraced(gs.time.c0);
untraced(gs.time.c1);
color(gs.time.c1, gold);
// ── 6 · BALANCE ──────────────────────────────────────────────────────────────
balance(rx, (640, 288), "Fe + O2 -> Fe2O3", 54);
tally(rx, (640, 470), 250, 42, 24);
hidden(rx);
hidden(rx.tally);
// ── 7 · SOLUTION ─────────────────────────────────────────────────────────────
lattice(salt, "NaCl", (600, 322), 6, 5, 50);
hidden(salt);
hidden(salt.captions);
// ── 8 · CURRENT ──────────────────────────────────────────────────────────────
cell(cl, "Zn|Cu", (640, 300), 640, 280, "resistance=10 carriers=12");
hidden(cl);
hidden(cl.captions);
// ── 9 · ROTATION ─────────────────────────────────────────────────────────────
newman(nm, "asset:molecules/butane.sdf", (330, 350), 150, 18);
profile(pf, nm, (900, 350), 480, 190);
hidden(nm);
hidden(pf);
// ═══════════════════════════════════════════════════════════════════════════
// the run
// ═══════════════════════════════════════════════════════════════════════════
// 0 · A MOLECULE — already there; the heteroatoms are what the rest of it hangs off
wait(1.4);
par { pulse(caf.O); pulse(caf.N); }
wait(1.0);
par { recolor(caf.O, coral, 0.6); recolor(caf.N, cyan, 0.6); }
wait(1.6);
// 1 · LEWIS — count, connect, complete, and then the bond that will not stay still
par { fade(caf, 0.7); fade(t0, 0.5); }
par { show(lw, 0.5); show(tl, 0.5); }
octet(lw, 5.5);
wait(0.6);
resonate(lw, 5.0, 2);
wait(1.0);
// 2 · MECHANISM — a bond made, a bond broken, and the charge leaving with the bromide
par { fade(lw, 0.6); fade(tl, 0.4); }
par { show(sub, 0.7); show(nuc, 0.7); show(t1, 0.5); }
wait(0.6);
par { draw(att, 0.9); pulse(nuc.O); }
wait(0.5);
par { draw(go, 0.8); pulse(sub.Br); }
wait(0.9);
draw(rxn, 0.6);
par { show(pro, 0.7); show(lea, 0.7); }
par { recolor(lea.Br, coral, 0.5); pulse(lea.Br); }
wait(1.6);
// 2 · LIGHT — the ladder, then the falls and the colours they make
par { fade(sub, 0.5); fade(nuc, 0.5); fade(pro, 0.5); fade(lea, 0.5); fade(att, 0.4); fade(go, 0.4); fade(rxn, 0.4); fade(t1, 0.4); }
par { show(lv, 0.8); show(t2, 0.5); }
wait(0.5);
show(spec, 0.7);
wait(0.4);
drop(lv, 3, 2, 1.6);
wait(0.3);
drop(lv, 4, 2, 1.4);
wait(0.3);
drop(lv, 6, 2, 1.4);
wait(0.4);
drop(lv, 2, 1, 1.8);
wait(1.2);
// 3 · MATTER — a molecule, and the three ways it can move
par { fade(lv, 0.6); fade(spec, 0.6); fade(t2, 0.4); }
par { show(h2o, 0.7); show(t3, 0.5); }
wait(0.4);
vibrate(h2o, 1, 1.8);
par { show(irw, 0.7); vibrate(h2o, 2, 1.6); }
vibrate(h2o, 3, 1.6);
wait(1.4);
// 4 · SILENCE — the same again, and the mode that leaves no peak
par { fade(h2o, 0.5); fade(irw, 0.5); fade(t3, 0.4); }
par { show(co2, 0.6); show(t4, 0.5); }
wait(0.3);
vibrate(co2, 1, 1.4);
par { show(irc, 0.6); vibrate(co2, 4, 1.4); }
wait(0.8);
par { pulse(irc.silent); }
vibrate(co2, 3, 2.6);
wait(1.4);
// 5 · COLLISION — molecules meet, and only the hard meetings count
par { fade(co2, 0.5); fade(irc, 0.5); fade(co2.readout, 0.4); fade(t4, 0.4); }
par { show(gs.box, 0.5); show(gs.particles, 0.6); show(t5, 0.5); }
wait(0.3);
par {
run(gs, 9);
draw(gs.time.c0, 9);
draw(gs.time.c1, 9);
seq {
show(gs.speeds.axis, 0.4);
show(gs.speeds.bars, 0.5);
wait(1.0);
draw(gs.speeds.mb, 1.2);
wait(0.8);
show(gs.time.frame, 0.4);
}
}
wait(1.2);
// 6 · BALANCE — atoms are conserved, and here is what that costs
par { fade(gs.box, 0.5); fade(gs.particles, 0.5); fade(gs.speeds, 0.5); fade(gs.time, 0.5); fade(t5, 0.4); }
par { show(rx, 0.6); show(t6, 0.5); }
wait(0.4);
show(rx.tally, 0.5);
wait(1.0);
solve(rx, 2.8);
wait(1.6);
// 7 · SOLUTION — a solid comes apart, corner first, and the water turns round
par { fade(rx, 0.5); fade(rx.tally, 0.5); fade(t6, 0.4); }
par { show(salt, 0.7); show(t7, 0.5); }
wait(0.8);
dissolve(salt, 7, 8);
wait(1.6);
// 8 · CURRENT — the same electrons, sent round a wire
par { fade(salt, 0.7); fade(t7, 0.4); }
par { show(cl, 0.8); show(t8, 0.5); }
wait(0.8);
discharge(cl, 6, 30);
wait(1.4);
// 9 · ROTATION — a bond turns, and rides the energy it costs
par { fade(cl, 0.6); fade(t8, 0.4); }
par { show(nm, 0.6); show(t9, 0.5); }
wait(0.4);
show(pf, 0.7);
wait(0.8);
twist(nm, 120, 1.3);
twist(nm, 60, 1.1);
twist(nm, 0, 1.4);
wait(0.8);
twist(nm, 300, 1.3);
twist(nm, 180, 1.3);
wait(1.4);
// coda — the molecule it opened on, with everything it is made of now lit
par { fade(nm, 0.6); fade(pf, 0.6); fade(t9, 0.4); }
par { show(caf, 0.9); show(t0, 0.6); }
wait(0.5);
par { pulse(caf.O); pulse(caf.N); }
wait(2.6);
lewis-structures
The bookkeeping, worked rather than copied. From a formula and nothing else: count the valence electrons, pick the central atom, spend two on every bond, complete the octets from the outside in, and — if the middle atom is still short — take a lone pair off a neighbour and make it a second bond. The formal charges then fall out, and they have to sum to the ion’s charge or the structure is refused. Two steps are why it is animated: the pair that SWINGS IN to finish carbon’s octet in CO₂ is a move rather than a picture, and resonance is a claim no single drawing can make — nitrate’s double bond is not on one oxygen, it is on all three at once, so it keeps moving while the lone pairs and charges follow it. Where it declines it declines for chemical reasons: nitric oxide has an odd number of valence electrons and cannot be drawn with every electron paired.
// Lewis structures — the bookkeeping, worked rather than copied
//
// It opens on four of them already drawn, because that is what the idea looks like: a single bond, a
// double, a triple, and an ion carrying a charge. Each one is labelled with its own formula by the
// builtin — that much is computable — and with its NAME by this scene, because "water" is a fact
// about usage rather than about the molecule. Every one is DERIVED from its formula rather than
// looked up — count the valence electrons, pick the central atom, spend two on every bond, complete
// the octets from the outside in, and if the middle atom is still short, take a lone pair off a
// neighbour and make it a second bond. The formal charges then fall out, and they have to sum to the
// ion's charge or the structure is refused.
//
// The middle act is the working itself, in the order a course teaches it, and the last is the claim
// no single drawing can make: nitrate's double bond is not on one oxygen, it is on all three at
// once, so it keeps moving while the lone pairs and the charges follow it.
//
// Change a formula in the source and its whole diagram changes — the letters, the lines, the dots
// and the charges are all one calculation.
title("lewis structures");
canvas("16:9");
template("black");
text(brand, (640, 26), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
// ── the title, and four examples standing there from the first frame ─────────
text(head, (640, 74), "Lewis Structures");
size(head, 34);
bold(head);
color(head, fg);
lewis(wat, "H2O", (300, 262), 92, 28);
lewis(cdi, "CO2", (960, 262), 96, 28);
lewis(hcn, "HCN", (300, 560), 96, 28);
lewis(amm, "NH4+", (960, 556), 84, 28);
text(l1, (300, 352), "water — one pair per bond, two left over");
size(l1, 16);
color(l1, dim);
text(l2, (960, 352), "carbon dioxide — two pairs per bond, where an octet needs it");
size(l2, 16);
color(l2, dim);
text(l3, (300, 666), "hydrogen cyanide — three pairs, when that is what it takes");
size(l3, 16);
color(l3, dim);
text(l4, (960, 666), "ammonium — and the charge is what the counting leaves over");
size(l4, 16);
color(l4, dim);
// ── the working, on the one where a pair has to become a bond ──
lewis(big, "CO2", (640, 320), 165, 38);
hidden(big);
text(why, (640, 500), "carbon is short of an octet, so a pair swings in — twice");
size(why, 20);
color(why, gold);
hidden(why);
text(why2, (640, 540), "carbon dioxide: count · connect · complete · then check the charges");
size(why2, 18);
color(why2, dim);
hidden(why2);
// ── and the one no single drawing can say ──
lewis(nit, "NO3-", (410, 350), 145, 34);
hidden(nit);
text(res, (940, 306), "nitrate: the double bond is not on one oxygen —");
size(res, 20);
color(res, fg);
hidden(res);
text(res2, (940, 342), "it is on all three at once, and the");
size(res2, 20);
color(res2, fg);
hidden(res2);
text(res3, (940, 378), "charges move with it");
size(res3, 20);
color(res3, coral);
hidden(res3);
// ── the beats ──
// 1 · the four examples are already on screen; let them be read, then point at the dots
wait(2.2);
par { pulse(wat.pairs); pulse(cdi.pairs); }
wait(0.6);
par { pulse(hcn.pairs); pulse(amm.charges); }
wait(2.0);
// 2 · the working, on carbon dioxide
par {
fade(wat, 0.6); fade(cdi, 0.6); fade(hcn, 0.6); fade(amm, 0.6);
fade(l1, 0.5); fade(l2, 0.5); fade(l3, 0.5); fade(l4, 0.5);
}
par { show(big, 0.5); show(why2, 0.5); }
octet(big, 6.5);
wait(0.4);
show(why, 0.5);
wait(2.2);
// 3 · resonance, where the answer will not hold still
par { fade(big, 0.5); fade(why, 0.4); fade(why2, 0.4); }
show(nit, 0.6);
octet(nit, 5.5);
wait(0.5);
par { show(res, 0.4); show(res2, 0.4); }
wait(0.7);
show(res3, 0.4);
resonate(nit, 7.5, 3);
wait(2.4);
hydrogen-spectrum
Four coloured lines, and every one of them computed. The rungs are placed at Eₙ = −13.606/n² eV (which is why they crowd towards zero), a drop from n to m releases exactly that difference, and λ = hc/ΔE turns it into a wavelength — then each spectral line’s COLOUR is computed from its own wavelength, so Balmer α is red because 656 nm is red rather than because red was chosen. The last beat is the transition with no line at all: 2 → 1 is the biggest jump in the diagram, and its 10.2 eV photon lands at 121 nm, which is ultraviolet. One electron only, on purpose — the Rydberg formula is exact for hydrogen and wrong for anything with two electrons to repel each other.
// The hydrogen spectrum — the colour is the arithmetic
//
// Four coloured lines is all a hydrogen discharge tube gives you, and every one of them is on
// screen for a computed reason. The levels are E_n = -13.606/n² eV, so the rungs are placed at
// their energies (which is why they crowd towards zero); a jump from n to m releases exactly that
// energy difference; and λ = hc/ΔE turns it into a wavelength. The COLOUR of each spectral line is
// then computed from its own wavelength — nothing is chosen, so Balmer alpha is red because 656 nm
// is red.
//
// The last beat is the one a spectrum can't show: the 2 → 1 drop releases 10.2 eV at 121 nm, which
// is ultraviolet. It is the biggest jump in the diagram and it leaves no line at all, because the
// eye's range is a fact about the eye rather than about hydrogen.
title("the hydrogen spectrum");
canvas("16:9");
template("black");
text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 66), "four lines, and every one of them computed");
size(head, 27);
bold(head);
color(head, fg);
hidden(head);
levels(lv, (300, 350), 250, 320, 6);
hidden(lv);
emission(sp, lv, (930, 300), 500, 96);
hidden(sp);
text(rule, (930, 470), "λ = hc/ΔE — the colour is the energy, converted");
size(rule, 18);
color(rule, fg);
hidden(rule);
text(uv, (930, 512), "n = 2 → 1 is the biggest jump of all, and leaves no line:");
size(uv, 18);
color(uv, coral);
hidden(uv);
text(uv2, (930, 540), "10.2 eV is 121 nm, and 121 nm is ultraviolet");
size(uv2, 18);
color(uv2, coral);
hidden(uv2);
text(foot, (640, 690), "one electron only — the Rydberg formula is exact for hydrogen and wrong for anything with two");
size(foot, 15);
color(foot, dim);
hidden(foot);
wait(0.4);
show(head, 0.6);
wait(0.3);
par { show(lv, 0.7); show(foot, 0.4); }
wait(0.5);
show(sp, 0.6);
wait(0.4);
drop(lv, 3, 2, 1.8);
wait(0.5);
drop(lv, 4, 2, 1.6);
wait(0.4);
drop(lv, 5, 2, 1.5);
wait(0.4);
drop(lv, 6, 2, 1.5);
wait(0.5);
show(rule, 0.5);
wait(2.0);
par { show(uv, 0.5); show(uv2, 0.5); }
drop(lv, 2, 1, 2.2);
wait(3.0);
galvanic-cell
The cell decides its own polarity. Give it two metals and the more positive standard reduction
potential becomes the cathode — so cell(c, "Cu|Zn") draws exactly the same cell, and both read
+1.10 V. From that one decision: E°cell = +0.34 − (−0.76), the half-equations the right way round,
and Ohm’s law across the 10 Ω resistor giving 0.110 A. The electrons and the salt-bridge ions move
at a rate set by that current, and the two counters are the exam question — after half an hour,
Q = It = 198 C, and Faraday turns that into the 67 mg of zinc the anode has lost. Change the
resistance in the source and every number moves.
// A galvanic cell — which way round, how many volts, and what it costs the zinc
//
// The cell is not told which electrode is which. Give it two metals and the more positive standard
// reduction potential is the one that gets reduced, so copper becomes the cathode and zinc the
// anode — `cell(c, "Cu|Zn")` would draw exactly the same cell. From that one decision everything
// else follows: E°cell = +0.34 − (−0.76) = 1.10 V, the half-equations are written the right way
// round, and Ohm's law across the 10 Ω external resistor sets the current at 0.110 A.
//
// The electrons and the salt-bridge ions move at a rate set by that current, and the two counters
// are the exam question: after half an hour, Q = It = 198 C, and Faraday's law turns that into the
// zinc the anode has lost, m = MQ/nF = 67 mg. Change the resistance in the source and every one of
// those numbers moves.
title("a galvanic cell, solved");
canvas("16:9");
template("paper");
text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 68), "the cell decides its own polarity");
size(head, 28);
bold(head);
color(head, ink);
hidden(head);
cell(c, "Zn|Cu", (640, 288), 660, 280, "resistance=10 carriers=10");
hidden(c);
hidden(c.captions);
text(why, (640, 640), "zinc is the more negative half-cell, so zinc is oxidised — that is the whole decision");
size(why, 18);
color(why, ink);
hidden(why);
text(law, (640, 674), "Q = It after half an hour, and m = MQ/nF is what the anode lost");
size(law, 18);
color(law, indigo);
hidden(law);
wait(0.4);
show(head, 0.6);
wait(0.3);
par { show(c, 0.8); show(c.captions, 0.8); }
wait(0.8);
show(why, 0.5);
wait(1.6);
show(law, 0.5);
discharge(c, 6, 30);
wait(3.0);
dissolving-salt
Why salt dissolves, and why the corners go first. Two computed things carry the lesson: the ORDER, from each ion’s coordination number (a corner has two neighbours holding it, an edge three, the middle four), so the crystal erodes inwards the way a real one does; and the ORIENTATION, from the sign of each charge — water turns its oxygen towards Na⁺ and its hydrogens towards Cl⁻, so the two kinds of hydration shell face opposite ways for a reason you can read off the screen. And the arithmetic underneath: +787 kJ/mol to pull the lattice apart, −770 back from hydrating the ions, so dissolving salt is very slightly ENDOTHERMIC and what drives it is entropy.
// Why salt dissolves — and why the corners go first
//
// Two things on screen are computed rather than choreographed, and they are the two things the
// lesson is about.
//
// The ORDER. Every ion's coordination number is counted from the lattice: a corner has two
// neighbours holding it, an edge three, an ion in the middle four. `dissolve` takes them in that
// order, so the crystal erodes inwards from its corners — which is what a crystal does, and why a
// cube of salt rounds off as it goes.
//
// The ORIENTATION. Water is a dipole, so it turns its oxygen towards a positive ion and its
// hydrogens towards a negative one. Each hydration shell here is built from the sign of the charge
// it is surrounding, so the sodium shells and the chloride shells face opposite ways — which is the
// picture of why water, specifically, is good at this.
//
// And the arithmetic underneath: pulling the lattice apart costs +787 kJ/mol, hydrating the two
// ions pays back −770, so dissolving salt is very slightly ENDOTHERMIC. It happens anyway, and what
// drives it is entropy. That surprises people, which is exactly why the numbers are on screen.
title("why salt dissolves");
canvas("16:9");
template("black");
text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 66), "a crystal comes apart where it is least held");
size(head, 27);
bold(head);
color(head, fg);
hidden(head);
lattice(x, "NaCl", (430, 306), 6, 5, 48);
hidden(x);
hidden(x.captions);
text(coord, (1010, 232), "coordination:");
size(coord, 20);
color(coord, fg);
hidden(coord);
text(coord2, (1010, 270), "corner 2 · edge 3 · inside 4");
size(coord2, 19);
color(coord2, cyan);
hidden(coord2);
text(dip, (1010, 336), "water is a dipole, so it turns round:");
size(dip, 19);
color(dip, fg);
hidden(dip);
text(dip2, (1010, 370), "oxygen towards Na⁺, hydrogens towards Cl⁻");
size(dip2, 19);
color(dip2, magenta);
hidden(dip2);
text(ent, (1010, 436), "and it is barely downhill at all —");
size(ent, 19);
color(ent, fg);
hidden(ent);
text(ent2, (1010, 470), "what drives it is entropy, not energy");
size(ent2, 19);
color(ent2, gold);
hidden(ent2);
wait(0.4);
show(head, 0.6);
wait(0.3);
show(x, 0.8);
wait(0.6);
par { show(coord, 0.4); show(coord2, 0.5); }
wait(1.2);
par { show(dip, 0.4); show(dip2, 0.5); }
wait(0.6);
par { show(x.captions, 0.4); dissolve(x, 7, 8); }
wait(0.6);
par { show(ent, 0.4); show(ent2, 0.5); }
wait(3.0);
butane-conformers
A single bond turns — but not for free. The bond is chosen for you (the one with the most on both sides, which for butane is the middle C–C), and the whole energy profile is computed by rotating the real geometry degree by degree: a threefold torsional term for the intrinsic preference for staggered, plus Lennard-Jones repulsion between the atoms on opposite sides, which is what makes two methyls mind each other far more than two hydrogens do. Out comes the textbook curve — staggered wells, anti lowest, the methyls eclipsing worst — and the projection and the marker are one calculation, so the molecule and the graph can never disagree. It is a rigid scan, so the peaks read high: the order and the shape are what to take from it.
// Butane's conformers — a Newman projection with the energy it costs
//
// The bond is not chosen by hand: `newman` looks for the single bond with the most on both sides,
// which for butane is the middle C–C. Then it computes the whole profile by ROTATING the real
// geometry degree by degree and scoring each arrangement — a threefold torsional term for the
// intrinsic preference for staggered, plus Lennard-Jones repulsion between the atoms on opposite
// sides, which is what makes two methyl groups mind each other far more than two hydrogens do.
//
// Out of that comes the curve every course draws: staggered wells at 60°, 180° and 300°, the anti
// conformer lowest, eclipsed peaks between them, and the worst arrangement at 0° where the two
// methyls are on top of each other. The projection and the marker are one calculation, so the
// molecule and the graph can never disagree.
//
// It is a rigid scan — the bond turns and nothing else moves — so the peaks come out high: a real
// butane widens its bond angles a little to relieve the syn contact, which is why the measured
// barrier is 19–21 kJ/mol and this one is in the fifties. The order and the shape are what to read.
title("butane: the cost of turning a bond");
canvas("16:9");
template("black");
text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 66), "a single bond turns — but not for free");
size(head, 27);
bold(head);
color(head, fg);
hidden(head);
newman(nm, "asset:molecules/butane.sdf", (300, 330), 150, 18);
hidden(nm);
profile(pf, nm, (880, 330), 500, 200);
hidden(pf);
text(foot, (640, 686), "rigid scan: the peaks come out high because a real molecule widens its angles to relieve them");
size(foot, 15);
color(foot, dim);
hidden(foot);
wait(0.4);
show(head, 0.6);
wait(0.3);
show(nm, 0.7);
wait(0.6);
par { show(pf, 0.7); show(foot, 0.4); }
wait(1.2);
twist(nm, 120, 1.6);
wait(1.0);
twist(nm, 60, 1.4);
wait(1.2);
twist(nm, 0, 1.6);
wait(1.6);
twist(nm, 300, 1.6);
wait(1.0);
twist(nm, 180, 1.6);
wait(3.0);
ir-modes
A mode is its motion — and some motions are invisible. vibration reads the real 3-D
geometry, builds a harmonic force field of bond, angle and out-of-plane springs and
diagonalises the mass-weighted Hessian, so what you watch are the actual eigenvectors: water’s
three modes (3N−6), carbon dioxide’s four (3N−5, two of them a degenerate bend). The
wavenumbers land near the measured ones because the masses and force constants are real —
H–Cl at 2886 cm⁻¹ against a measured 2886, CO₂’s asymmetric stretch at 2374 against 2349. Then
the point: a vibration absorbs infrared only if it changes the dipole moment, and CO₂’s
symmetric stretch does not — both oxygens move out together, the bond dipoles stay equal and
opposite, and the spectrum has no peak there however hard the bond is vibrating. The scene
animates that silent mode against its own gap in the spectrum. The cancellation is computed from
the mode’s displacements, not asserted.
// Infrared: a mode is its motion, and some motions are invisible
//
// Nothing here is drawn by hand or looked up. `vibration` reads the real 3-D geometry out of the
// structure file, builds a harmonic force field of bond, angle and out-of-plane springs with
// tabulated force constants, and diagonalises the mass-weighted Hessian. What comes back is the
// textbook set of modes — 3N−6 of them, or 3N−5 when the molecule is a straight line — each with a
// wavenumber and its own eigenvector, which is what `vibrate` animates.
//
// The numbers land where a spectroscopy table says they should, because the masses and the force
// constants are real: H–Cl comes out at 2886 cm⁻¹ against a measured 2886, and carbon dioxide's
// asymmetric stretch at 2374 against 2349. It is a harmonic model, so treat a wavenumber as good to
// within a hundred or so — anharmonicity, Fermi resonance and overtones are all outside it.
//
// The point of the scene is the mode that ISN'T there. A vibration absorbs infrared only if it
// changes the molecule's dipole moment, and carbon dioxide's symmetric stretch does not: both
// oxygens move out together, the two bond dipoles stay equal and opposite, and the spectrum has no
// peak however hard the bond is vibrating. That cancellation is computed — the intensity is
// |Σ qᵢ·dᵢ|² over the mode's own displacements — so the silent mode is silent for the reason a
// chemist would give, not because a table said so.
title("infrared: the mode that isn't there");
canvas("16:9");
template("black");
text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 66), "a vibration is only visible if the dipole moves");
size(head, 28);
bold(head);
color(head, fg);
hidden(head);
// ── water: three modes, all of them active ──
vibration(h2o, "asset:molecules/water.sdf", (330, 330), 130, 22);
hidden(h2o);
irspectrum(ws, h2o, (930, 320), 540, 170, 15);
hidden(ws);
text(wlab, (330, 150), "water — bent, so 3N−6 = 3 modes");
size(wlab, 20);
color(wlab, fg);
hidden(wlab);
text(wsay, (900, 520), "three modes, three peaks — every one moves the dipole");
size(wsay, 18);
color(wsay, fg);
hidden(wsay);
// ── carbon dioxide: four modes, and one of them is silent ──
vibration(co2, "asset:molecules/carbon-dioxide.sdf", (330, 330), 130, 22);
hidden(co2);
irspectrum(cs, co2, (930, 320), 540, 170, 15);
hidden(cs);
text(clab, (330, 150), "carbon dioxide — linear, so 3N−5 = 4 modes");
size(clab, 20);
color(clab, fg);
hidden(clab);
text(csay, (900, 520), "four modes — and only three peaks");
size(csay, 19);
color(csay, fg);
hidden(csay);
text(cwhy, (900, 556), "the symmetric stretch moves both oxygens out together —");
size(cwhy, 17);
color(cwhy, coral);
hidden(cwhy);
text(cwhy2, (900, 582), "the bond dipoles stay equal and opposite, so nothing absorbs");
size(cwhy2, 17);
color(cwhy2, coral);
hidden(cwhy2);
text(foot, (640, 690), "harmonic model, tabulated force constants — a wavenumber is good to about a hundred");
size(foot, 15);
color(foot, dim);
hidden(foot);
// ── ACT 1: water, mode by mode ──
wait(0.4);
show(head, 0.6);
wait(0.3);
par { show(h2o, 0.6); show(wlab, 0.4); }
wait(0.4);
vibrate(h2o, 1, 2.4); // the scissor bend
wait(0.4);
vibrate(h2o, 2, 2.4); // symmetric stretch
wait(0.4);
vibrate(h2o, 3, 2.4); // asymmetric stretch
wait(0.5);
par { show(ws, 0.7); show(foot, 0.4); }
wait(0.5);
show(wsay, 0.5);
wait(2.4);
// ── ACT 2: carbon dioxide, where one mode goes missing ──
par { fade(h2o, 0.5); fade(ws, 0.5); fade(wlab, 0.4); fade(wsay, 0.4); }
par { show(co2, 0.6); show(clab, 0.4); }
wait(0.4);
vibrate(co2, 1, 2.2); // bend
wait(0.3);
vibrate(co2, 4, 2.2); // asymmetric stretch
wait(0.4);
show(cs, 0.7);
wait(0.6);
show(csay, 0.5);
wait(1.6);
// the one with no peak — animate it against its own gap in the spectrum
par { show(cwhy, 0.5); pulse(cs.silent); }
vibrate(co2, 3, 3.2); // the symmetric stretch: nothing absorbs
show(cwhy2, 0.5);
wait(3.0);
limiting-reagent
The one that runs out is not the one you have least of. 10.0 g of iron and 5.0 g of oxygen:
there is less oxygen by mass and fewer MOLES of oxygen too, and iron is still what runs out
first — because the comparison that decides it is not the amount but the amount DIVIDED BY THE
COEFFICIENT (0.0448 batches against 0.0521). Every number is computed: balance solves the
coefficients, supply converts grams to moles with the standard atomic weights — the ones you
weigh with, not the monoisotopic masses a mass spectrum uses — and react grows the bars, names
the loser and counts the yields up. It closes on the check any stoichiometry answer has to pass:
15.00 g in, 14.30 g of oxide plus 0.70 g of unused oxygen out. Change 10 g to 20 g in the source
and the bars, the winner and every mass move on their own.
// The limiting reagent — the one that runs out is not the one you have least of
//
// 10.0 g of iron and 5.0 g of oxygen. There is less oxygen by mass, and there are fewer MOLES of
// oxygen too (0.156 against 0.179) — and iron is still what runs out first. That is the whole
// lesson, and it is the reason the question is worth asking at all: the comparison that decides it
// is not the amount, it is the amount DIVIDED BY THE COEFFICIENT.
//
// Fe: 0.1791 mol / 4 = 0.0448 batches ← runs out first
// O2: 0.1563 mol / 3 = 0.0521 batches
//
// Every number here is computed. `balance` solves the coefficients (as the null space of the atom
// matrix), `supply` converts grams to moles with the STANDARD atomic weights — the ones you weigh
// with, not the monoisotopic masses a mass spectrum uses, which differ by more than rounding —
// and `limiting` + `react` do the comparison and count the answer up. Change 10 g to 20 g in the
// source and the bars, the winner and every mass on screen move on their own.
//
// The last line is the check any stoichiometry answer has to pass: 15.00 g of reagents in, and
// 14.30 g of oxide plus 0.70 g of unused oxygen out. Nothing was created, and nothing was lost —
// which is the same claim the balanced equation was making, now in grams.
title("the limiting reagent");
canvas("16:9");
template("paper");
text(brand, (640, 30), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 74), "which one runs out first?");
size(head, 30);
bold(head);
color(head, ink);
hidden(head);
// ── the reaction, balanced first because the coefficients are the whole point ──
balance(rx, (640, 168), "Fe + O2 -> Fe2O3", 44);
hidden(rx);
supply(rx, "Fe=10g O2=5g");
text(given, (640, 246), "10.0 g of iron, 5.0 g of oxygen");
size(given, 22);
color(given, ink);
hidden(given);
text(guess, (640, 290), "there is less oxygen — by mass AND by moles. So oxygen runs out?");
size(guess, 19);
color(guess, dim);
hidden(guess);
// ── the comparison that actually decides it ──
limiting(rx, (640, 470), 660, 52, 21);
hidden(rx.limit);
text(why, (640, 692), "moles ÷ coefficient — four irons are needed per batch, and only three oxygens");
size(why, 19);
color(why, indigo);
hidden(why);
text(check, (640, 692), "15.00 g in, 15.00 g out — the balanced equation, now in grams");
size(check, 19);
color(check, ink);
hidden(check);
// ── the beats ──
wait(0.4);
show(head, 0.6);
wait(0.3);
show(rx, 0.5);
wait(0.4);
solve(rx, 1.8);
wait(0.6);
show(given, 0.5);
wait(0.8);
show(guess, 0.5);
wait(2.2);
// the bars settle the question
par { fade(guess, 0.4); show(rx.limit, 0.5); }
react(rx, 3.4);
wait(0.4);
show(why, 0.6);
wait(2.6);
// and the answer checks itself
fade(why, 0.4);
show(check, 0.6);
wait(3.0);
balancing
Conservation, worked rather than asserted. balance reads the skeleton equation, counts the
atoms in every formula and solves the coefficients; tally keeps score element by element;
solve writes the coefficients in one at a time and the counts follow, so a half-finished state
is a real one — a species with no coefficient yet counts as one of itself, exactly the arithmetic
a student is doing mid-question. The coefficients are not guessed: one column per species and one
row per element gives a matrix whose NULL SPACE is precisely the set of balanced equations, found
by exact rational elimination (no floating point — the answer is an integer vector) and reduced to
the smallest whole numbers, which is why 4/3/2 comes out and not 8/6/4. The animation is
rewrite, so each coefficient slides in beside a formula that stays exactly where it is. Then the
reason a solver earns its place: 2 KMnO₄ + 3 H₂SO₄ + 5 H₂C₂O₄ → K₂SO₄ + 2 MnSO₄ + 10 CO₂ + 8 H₂O,
which nobody balances by inspection.
// Balancing an equation — conservation, worked rather than asserted
//
// Nothing here is placed by hand. `balance` reads the skeleton equation, counts the atoms in each
// formula, and solves for the coefficients; `tally` keeps score element by element; `solve` writes
// the coefficients in one at a time, and every count on the board updates as it goes. The half-done
// states are real half-done states — a species without its coefficient yet counts as one of itself,
// which is exactly the arithmetic a student is doing mid-question.
//
// The coefficients are not guessed and not searched for. One column per species and one row per
// element gives a matrix whose NULL SPACE is precisely the set of balanced equations:
//
// Fe + O2 -> Fe2O3 Fe [ 1 0 -2 ] (4, 3, 2)ᵀ = 0
// O [ 0 2 -3 ]
//
// so balancing is exact rational elimination — no floating point anywhere, because the answer is an
// integer vector and a float could only ever say "about 4". The result is then reduced to the
// SMALLEST whole numbers, which is why 4/3/2 comes out rather than 8/6/4.
//
// The animation is `rewrite`: each coefficient slides in beside a formula that stays exactly where
// it is, because equal glyphs keep their identity across a rewrite. The equation is never retyped
// and never re-laid-out — which is the difference between watching an equation get balanced and
// watching one picture get replaced by another.
//
// The second half is the reason a solver earns its place: nobody balances permanganate against
// oxalic acid by inspection.
title("balancing: conservation as arithmetic");
canvas("16:9");
template("paper");
text(brand, (640, 30), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 74), "an equation is a claim about atoms");
size(head, 30);
bold(head);
color(head, ink);
hidden(head);
// ── ACT 1–3: the one everybody meets first ──
balance(rx, (640, 210), "Fe + O2 -> Fe2O3", 54);
hidden(rx);
tally(rx, (392, 452), 250, 40, 24);
hidden(rx.tally);
text(tallywhy, (392, 566), "atoms in, atoms out");
size(tallywhy, 17);
color(tallywhy, dim);
hidden(tallywhy);
text(notyet, (640, 320), "iron and oxygen do not balance — so this is not an equation yet");
size(notyet, 19);
color(notyet, crimson);
hidden(notyet);
// the working, written out: the coefficients ARE the null space of the atom matrix
equation(mx, (912, 452), `\begin{bmatrix}1 & 0 & -2\\ 0 & 2 & -3\end{bmatrix}\begin{pmatrix}4\\3\\2\end{pmatrix}=\begin{pmatrix}0\\0\end{pmatrix}`, 34);
color(mx, indigo);
hidden(mx);
text(mxwhy, (912, 566), "one row per element — the coefficients are its null space");
size(mxwhy, 16);
color(mxwhy, dim);
hidden(mxwhy);
text(done1, (640, 320), "every atom accounted for — and these are the smallest whole numbers");
size(done1, 19);
color(done1, ink);
hidden(done1);
// ── ACT 4: the one nobody balances by eye ──
balance(ox, (640, 214), "KMnO4 + H2SO4 + H2C2O4 -> K2SO4 + MnSO4 + CO2 + H2O", 32);
hidden(ox);
tally(ox, (392, 466), 250, 34, 22);
hidden(ox.tally);
text(oxlab, (640, 300), "the permanganate–oxalate titration, seven species and six elements");
size(oxlab, 18);
color(oxlab, dim);
hidden(oxlab);
text(oxwhy, (912, 466), "By inspection this takes a page.\nAs a null space it is one answer,\nand it is exact.");
size(oxwhy, 19);
color(oxwhy, ink);
hidden(oxwhy);
text(close, (640, 640), "conservation is not a rule you apply — it is the equation that has to hold");
size(close, 20);
color(close, ink);
hidden(close);
// ── the beats ──
wait(0.4);
show(head, 0.6);
wait(0.3);
show(rx, 0.6);
wait(0.5);
par { show(rx.tally, 0.5); show(tallywhy, 0.4); }
wait(0.8);
show(notyet, 0.5);
wait(1.8);
// the coefficients go in, one at a time, and the tally follows them
fade(notyet, 0.4);
solve(rx, 3.0);
wait(0.6);
show(done1, 0.5);
wait(1.6);
// and where those numbers came from
par { show(mx, 0.6); show(mxwhy, 0.4); }
wait(3.0);
// ── the hard one ──
par {
fade(rx, 0.5);
fade(rx.tally, 0.5);
fade(done1, 0.4);
fade(mx, 0.4);
fade(mxwhy, 0.4);
fade(tallywhy, 0.4);
}
par { show(ox, 0.6); show(oxlab, 0.4); }
wait(0.6);
par { show(ox.tally, 0.5); show(oxwhy, 0.5); }
wait(1.0);
solve(ox, 4.2);
wait(0.8);
show(close, 0.6);
wait(3.0);
collision-theory
Why warming it up speeds it up — measured, not asserted. The same mixture over the same
barrier at two temperatures, and every number on screen comes off one trajectory: gas bounces
72 hard discs elastically, rule(cold, "A + B -> C + C when energy > 3.2") says what happens
when two of them meet hard enough, and that is the whole model. The histogram is a MEASUREMENT —
speeds bins the gas’s own speeds while it runs and the gold line over it is the exact
Maxwell–Boltzmann curve for that temperature, so they agree because they are the same gas. The
threshold is in the same unit as the temperature, which makes Ea/kT the Boltzmann exponent: 4% of
collisions clear the barrier at kT = 1.0 and 28% at kT = 2.5, and for 2-D hard discs that
fraction is exp(-Ea/kT) exactly — reproduced by the sim rather than imposed on it. The cold
gas’s tail cannot reach the line; the hot one spills past it. Nothing in the primitive knows the
word chemistry: the same rule with no when clause is an epidemic ("S + I -> I").
// Collision theory — why warming it up speeds it up
//
// Two runs of the SAME mixture over the SAME barrier, at two temperatures. Nothing on screen is
// choreographed: `gas` integrates 72 hard discs, `rule` says what happens when two of them meet
// hard enough, and everything else — how often that happens, how fast the product appears, what
// shape the speeds take — is measured off that one trajectory.
//
// rule(cold, "A + B -> C + C when energy > 3.2")
//
// The threshold is the relative kinetic energy along the line of centres, in the same unit as
// `temperature`, so Ea/kT is exactly the Boltzmann exponent. For 2-D hard discs the fraction of
// collisions that clear a barrier is exp(-Ea/kT) — and the engine reproduces that to within two
// percentage points across four (T, Ea) pairs, which is what
// `the_fraction_of_collisions_over_the_barrier_is_the_boltzmann_factor` measures. So:
//
// kT = 1.0 exp(-3.2/1.0) = 4% of collisions react
// kT = 2.5 exp(-3.2/2.5) = 28% of collisions react
//
// 2.5x the temperature, and 7x the fraction over the barrier — plus faster molecules colliding
// more often on top of that. That double effect is the whole lesson, and here it is arithmetic
// rather than assertion.
//
// The histogram is a MEASUREMENT: `speeds` bins the gas's own speeds every few frames while it
// runs, and the gold line over it is the exact Maxwell-Boltzmann curve for that temperature, on the
// same scale. They agree because they are the same gas. (Each panel is normalised to its own peak,
// so read the SHAPE and where the tail reaches, not bar height between panels.)
//
// Both panels share one speed axis (`vmax` on `speeds`), so the cold gas visibly cannot reach where
// the hot one lives.
title("collision theory: the barrier and the tail");
canvas("16:9");
template("black");
text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 62), "A + B → C, over a barrier");
size(head, 30);
bold(head);
color(head, fg);
hidden(head);
text(sub, (640, 96), "same mixture, same barrier, two temperatures");
size(sub, 19);
color(sub, fg);
hidden(sub);
// ── the two gases ──
//
// Identical in every respect but kT. Same seed, so they even start from the same arrangement — the
// only difference between the two runs is how hard the discs are moving.
gas(cold, (348, 404), 460, 344, 72, "temperature=1.0 radius=7 steps=400 seed=5");
species(cold, A, 0.5, cyan);
species(cold, B, 0.5, magenta);
species(cold, C, gold);
rule(cold, "A + B -> C + C when energy > 3.2");
speeds(cold, (930, 300), 460, 150, 12, 13, 5.2);
gas(hot, (348, 404), 460, 344, 72, "temperature=2.5 radius=7 steps=400 seed=5");
species(hot, A, 0.5, cyan);
species(hot, B, 0.5, magenta);
species(hot, C, gold);
rule(hot, "A + B -> C + C when energy > 3.2");
speeds(hot, (930, 300), 460, 150, 12, 13, 5.2);
// Hide the parts, not the whole gas: `draw` animates a trace, not opacity, so a curve that is
// meant to be DRAWN must stay visible and untraced rather than hidden.
hidden(cold.box);
hidden(cold.particles);
hidden(cold.speeds.axis);
hidden(cold.speeds.bars);
untraced(cold.speeds.mb);
hidden(hot.box);
hidden(hot.particles);
hidden(hot.speeds.axis);
hidden(hot.speeds.bars);
untraced(hot.speeds.mb);
// ── the barrier, on the speed axis ──
//
// The panel runs 0 to 5.2 in sim speed units across 460 px from x = 700. A lone disc hitting a
// still partner head-on carries E = m*v^2/4, so v = 2*sqrt(Ea/m) = 3.58 is the speed that clears
// 3.2 by itself: x = 700 + 460*3.58/5.2 = 1016. That is what the line marks — one particular way to
// pay the barrier, and the honest label for it.
line(bar, (1016, 375), (1016, 228));
color(bar, gold);
stroke(bar, 2);
untraced(bar);
text(barlab, (1074, 214), "Eₐ = 3.2 kT");
size(barlab, 17);
color(barlab, gold);
hidden(barlab);
text(barwhy, (1118, 246), "clears Eₐ alone");
size(barwhy, 14);
color(barwhy, fg);
hidden(barwhy);
text(spdlab, (930, 398), "speed → (measured bars, Maxwell–Boltzmann line)");
size(spdlab, 16);
color(spdlab, fg);
hidden(spdlab);
// ── the two reaction curves ──
//
// `timegraph` is the generic sim view, and a gas's state variables are its populations — so this is
// the reactant falling and the product rising, with no chemistry-specific vocabulary. Drawn with
// `draw` over the run's own duration, so the curve arrives exactly as the collisions happen.
timegraph(cold, (818, 570), 96);
timegraph(hot, (1046, 570), 96);
hidden(cold.time.frame);
hidden(cold.time.title);
hidden(cold.time.sweep);
hidden(hot.time.frame);
hidden(hot.time.title);
hidden(hot.time.sweep);
color(cold.time.c1, gold);
color(hot.time.c1, gold);
untraced(cold.time.c0);
untraced(cold.time.c1);
untraced(hot.time.c0);
untraced(hot.time.c1);
text(coldlab, (818, 458), "kT = 1.0");
size(coldlab, 16);
color(coldlab, cyan);
hidden(coldlab);
text(hotlab, (1046, 458), "kT = 2.5");
size(hotlab, 16);
color(hotlab, magenta);
hidden(hotlab);
text(mix, (348, 598), "36 A + 36 B, elastic discs");
size(mix, 16);
color(mix, fg);
hidden(mix);
// ── the readings, one per act ──
text(read1, (348, 636), "4% of collisions clear Eₐ");
size(read1, 22);
color(read1, cyan);
hidden(read1);
text(read2, (348, 636), "28% of collisions clear Eₐ");
size(read2, 22);
color(read2, magenta);
hidden(read2);
equation(bolt, (176, 168), `f=e^{-E_\mathrm{a}/kT}`, 26);
color(bolt, fg);
hidden(bolt);
text(point, (640, 690), "2.5× the temperature — 7× the fraction over the barrier, and more collisions besides");
size(point, 17);
color(point, fg);
hidden(point);
// ── ACT 1: a box of moving discs ──
wait(0.4);
par { show(head, 0.6); show(sub, 0.5); }
wait(0.4);
par { show(cold.box, 0.5); show(mix, 0.4); }
show(cold.particles, 0.6);
wait(0.4);
// ── ACT 2: the cold run. The histogram builds itself while the discs move. ──
par {
run(cold, 9);
draw(cold.time.c0, 9);
draw(cold.time.c1, 9);
seq {
show(cold.speeds.axis, 0.4);
par { show(cold.speeds.bars, 0.5); show(spdlab, 0.4); }
wait(1.2);
draw(cold.speeds.mb, 1.2);
wait(0.6);
par { draw(bar, 0.5); show(barlab, 0.4); }
show(barwhy, 0.4);
wait(0.8);
par { show(cold.time.frame, 0.4); show(coldlab, 0.4); }
wait(1.0);
par { show(read1, 0.5); show(bolt, 0.5); }
}
}
wait(1.4);
// ── ACT 3: same barrier, hotter gas. Only kT changed. ──
par { fade(cold.particles, 0.5); fade(cold.speeds.bars, 0.4); fade(cold.speeds.mb, 0.4); fade(read1, 0.4); }
par { show(hot.particles, 0.5); show(hot.speeds.bars, 0.4); }
wait(0.3);
par {
run(hot, 9);
draw(hot.time.c0, 9);
draw(hot.time.c1, 9);
seq {
draw(hot.speeds.mb, 1.0);
wait(0.6);
par { show(hot.time.frame, 0.4); show(hotlab, 0.4); }
wait(1.2);
show(read2, 0.5);
}
}
wait(0.8);
// ── ACT 4: the two curves, side by side ──
par { pulse(cold.time.c1); pulse(hot.time.c1); }
show(point, 0.6);
wait(3.0);
nmr-ethanol
A spectrometer sweeping, in hertz. One pen moves left to right and everything follows it:
the ink appears under the nib, the frequency readout runs, the camera pushes in on whichever
protons the pen has just reached, and their colour arrives on the turning molecule at the moment
their peak does. The whole rig hangs off ONE number — parameter(sw, …) is the sweep position,
and bind wires it to the ink, the δ readout and the Hz readout at once, so the figure on screen
is always the frequency the pen is actually over rather than a caption timed to look right. Then
it rewinds into the CH₂ peak and the scale changes underneath it: at 0–5 ppm a 7 Hz splitting is
0.0175 ppm, two pixels — it was never one line. The coda is the reason the unit matters. δ 3.70 is
1480 Hz at 400 MHz and 222 Hz at 60 MHz, but J stays 7.0 Hz at both, which is why high field
resolves. Nothing here simulates NMR: the trace is a sum of Lorentzians at literature shifts,
which is what a spectrometer’s output is, so the 3 : 2 : 1 integration falls out of the peak
areas instead of being asserted.
// Ethanol, ¹H NMR — a spectrometer sweeping, in hertz
//
// One pen, moving left to right, and everything else follows it: the ink appears under the nib, the
// frequency readout runs, the camera pushes in on whichever protons the pen has just reached, and
// their colour arrives on the molecule at the moment their peak does. Nothing is cross-cut — it is
// one continuous sweep, which is what a spectrometer actually does.
//
// NOTHING here simulates NMR. The trace is a sum of Lorentzian line shapes at literature chemical
// shifts, which is what a spectrometer's output IS, so the curve is computed and the integration
// ratio falls out of the peak areas rather than being asserted. Everything else is `molecule3`,
// `plot`, `parameter` + `bind`, `orbit3` and core verbs.
//
// THE WHOLE RIG HANGS OFF ONE NUMBER. `parameter(sw, …)` is the sweep position, and `bind` wires it
// to the ink (`trace`), the δ readout and the Hz readout. Animating `sw` moves all of them together
// and in step, so the number on screen is always the frequency the pen is actually over — not a
// caption timed to look right.
//
// bind(sw, trace, trace, "y/5") the ink follows the pen
// bind(sw, dread, value, "5-y") δ, counting down the reversed axis
// bind(sw, hread, value, "(5-y)*400") and the same position in hertz, at 400 MHz
//
// A binding formula receives the parameter as **y**, not x — it is evaluated as `node.eval(0, p)`.
// Using `x` silently freezes the readout at its initial value, which is a good hour lost.
//
// Values (CDCl₃, literature):
// CH₃ δ 1.22, triplet, J = 7.0 Hz, 3H
// CH₂ δ 3.70, quartet, J = 7.0 Hz, 2H
// OH δ 2.60, singlet, 1H — this one genuinely moves. The hydroxyl shift depends on
// concentration, temperature and how dry the solvent is,
// because the proton is exchanging; quoted values run
// from about 1.5 to 5. That is the chemistry, not sloppy
// data.
//
// THE AXIS RUNS BACKWARDS on purpose: an NMR spectrum puts δ = 0 on the RIGHT. The plots are written
// in `u = 5 − δ` and the ticks are labelled by hand with the ppm they stand for.
title("ethanol proton NMR");
canvas("16:9");
template("black");
text(brand, (640, 32), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
// ── the molecule, in its own viewport panel ──
camera3((0, -9.5, 3), (0, 0, 0), 38, perspective, (300, 330), 500, 470);
molecule3(mol, "asset:molecules/ethanol.sdf", (0, 0, 0), 1.7, "style=ball spin=16 axis=z");
// Which hydrogen is which, read off the file's own bond block: a0 is the oxygen, a1 the CH₂ carbon,
// a2 the CH₃ carbon — so a3/a4 are the CH₂ protons, a5/a6/a7 the CH₃ protons, a8 the hydroxyl.
text(mlab, (300, 616), "ethanol · CH₃CH₂OH");
size(mlab, 21); color(mlab, fg); hidden(mlab);
// ── the instrument readout: the number that runs ──
text(field, (1062, 96), "400 MHz");
size(field, 16); color(field, dim); hidden(field);
counter(dread, (1062, 138), 5, 2, "δ ", " ppm");
size(dread, 25); color(dread, fg); hidden(dread);
counter(hread, (1062, 190), 2000, 0, "", " Hz");
size(hread, 34); color(hread, cyan); hidden(hread);
// ── the spectrum ──
field(spec, "3/(1+((x-3.78)/0.035)^2) + 1/(1+((x-2.40)/0.035)^2) + 2/(1+((x-1.30)/0.035)^2)");
coords(ax, (672, 580), (0, 5), (0, 3.4), 110, 92, 1);
hidden(ax);
xtick(t0, ax, 0, "5"); xtick(t1, ax, 1, "4"); xtick(t2, ax, 2, "3");
xtick(t3, ax, 3, "2"); xtick(t4, ax, 4, "1"); xtick(t5, ax, 5, "0");
for i in 0..6 { hidden(t{i}); }
text(axlab, (947, 636), "δ / ppm");
size(axlab, 17); color(axlab, dim); hidden(axlab);
plot(trace, (672, 580), 110, 92, "spec(x,0)", (0, 5));
color(trace, cyan);
stroke(trace, 3);
untraced(trace);
// the pen: a nib riding the trace, and the drop line beneath it
curvedot(nib, trace, 0);
color(nib, gold);
size(nib, 7);
hidden(nib);
// A faint full-height sweep bar, so the pen has a leading edge to travel on. It is a `rect` and not
// a `line` on purpose: a line keeps its END point inside the shape and only its START in `pos`, so
// shifting one stretches it into a diagonal rather than sliding it across. A rect is centred on
// `pos` and moves rigidly.
rect(bar, (672, 421), 2, 318);
color(bar, dim);
opacity(bar, 0.30);
hidden(bar);
// ── the driver, and everything wired to it ──
parameter(sw, (1062, 700), 0, 0, 5, "sweep", 2);
hidden(sw);
bind(sw, trace, trace, "y/5");
bind(sw, dread, value, "5-y");
bind(sw, hread, value, "(5-y)*400");
// ── assignments, revealed as the pen reaches each one ──
text(lch2, (815, 366), "CH₂");
size(lch2, 20); color(lch2, gold); hidden(lch2);
text(sch2, (815, 390), "δ 3.70 · 2H");
size(sch2, 14); color(sch2, dim); hidden(sch2);
text(loh, (936, 458), "OH");
size(loh, 20); color(loh, crimson); hidden(loh);
text(soh, (936, 482), "δ 2.60 · 1H");
size(soh, 14); color(soh, dim); hidden(soh);
text(lch3, (1088, 274), "CH₃");
size(lch3, 20); color(lch3, cyan); hidden(lch3);
text(sch3, (1088, 298), "δ 1.22 · 3H");
size(sch3, 14); color(sch3, dim); hidden(sch3);
text(integ, (947, 224), "areas 3 : 2 : 1 — which is how many protons");
size(integ, 18); color(integ, fg); hidden(integ);
// ── and then, inside one peak ──
// Written in `v = Hz + 16` so the frame's ORIGIN sits at the left edge. Centring the origin on the
// multiplet puts the y-axis straight through the middle of it, which is unreadable — and a Hz-offset
// axis has no business having a y-axis in the middle anyway.
field(quartet, "1/(1+((x-5.5)/1.1)^2) + 3/(1+((x-12.5)/1.1)^2) + 3/(1+((x-19.5)/1.1)^2) + 1/(1+((x-26.5)/1.1)^2)");
// `step` is 7 — the coupling constant itself — so the ticks ARE the spacing being measured, and the
// four lines fall halfway between them. Left to auto-number, 33 integers arrive as one grey smear.
coords(zax, (711, 556), (0, 32), (0, 3.6), 15, 74, 1, 7, 0);
hidden(zax);
xtick(z1, zax, 2, "-14"); xtick(z2, zax, 9, "-7"); xtick(z3, zax, 16, "0");
xtick(z4, zax, 23, "+7"); xtick(z5, zax, 30, "+14");
for i in 1..6 { hidden(z{i}); }
plot(zq, (711, 556), 15, 74, "quartet(x,0)", (0, 32));
color(zq, gold); stroke(zq, 3); untraced(zq);
curvedot(znib, zq, 0);
color(znib, cyan);
hidden(znib);
text(zlab, (951, 616), "Hz from the centre of the CH₂ peak");
size(zlab, 16); color(zlab, dim); hidden(zlab);
// a second pen, in hertz, because that is the unit the splitting lives in
parameter(zsw, (1062, 700), 0, 0, 32, "hz", 1);
hidden(zsw);
counter(zread, (1062, 190), -16, 1, "", " Hz");
size(zread, 34); color(zread, gold); hidden(zread);
bind(zsw, zq, trace, "y/32");
bind(zsw, zread, value, "y-16");
// the coupling constant, measured between the two inner lines
line(jbar, (899, 300), (1004, 300));
color(jbar, fg); stroke(jbar, 2); untraced(jbar);
text(jlab, (951, 274), "J = 7.0 Hz");
size(jlab, 21); color(jlab, fg); hidden(jlab);
text(zwhy, (951, 224), "one peak — four lines");
size(zwhy, 21); color(zwhy, gold); hidden(zwhy);
// ── the coda ──
text(k1, (300, 604), "δ 3.70 is 1480 Hz at 400 MHz");
size(k1, 19); color(k1, cyan); hidden(k1);
text(k2, (300, 632), "and 222 Hz at 60 MHz");
size(k2, 19); color(k2, dim); hidden(k2);
text(k3, (300, 672), "J stays 7.0 Hz at both");
size(k3, 20); color(k3, gold); hidden(k3);
text(k4, (300, 700), "which is why high field resolves");
size(k4, 15); color(k4, dim); hidden(k4);
// ═══ ACT 1: the molecule, turning ═══
wait(0.5);
show(mlab, 0.7);
wait(1.0);
// ═══ ACT 2: the instrument comes up ═══
par { show(ax, 0.7); show(axlab, 0.5); show(field, 0.5); }
par { show(t0, 0.3); show(t1, 0.3); show(t2, 0.3); show(t3, 0.3); show(t4, 0.3); show(t5, 0.3); }
par { show(dread, 0.5); show(hread, 0.5); }
par { show(bar, 0.4); show(nib, 0.4); }
wait(0.6);
// ═══ ACT 3: the sweep ═══
//
// Broken into four legs so the pen can be met at each peak. The legs are proportional to the gaps
// between the peaks, so the pen travels at a CONSTANT rate the whole way across — a spectrometer
// does not slow down for the interesting parts.
//
// 5 ppm over 7.0 s = 1.4 s per ppm. Peaks sit at u = 1.30, 2.40, 3.78.
// leg 1 → the CH₂ peak at u 1.30
par {
to(sw, value, 1.30, 1.82); to(nib, x, 1.30, 1.82);
shift(bar, (143, 0), 1.82);
}
// the pen is on it: the CH₂ protons take the pen's colour, and the camera goes to look
par {
recolor(mol.a3, gold, 0.5); recolor(mol.a4, gold, 0.5);
orbit3(24, 20, 7.9, 0.9);
show(lch2, 0.4); show(sch2, 0.4);
}
wait(0.5);
// leg 2 → the hydroxyl at u 2.40
par {
to(sw, value, 2.40, 1.54); to(nib, x, 2.40, 1.54);
shift(bar, (121, 0), 1.54);
orbit3(-8, 16, 8.8, 1.4);
}
par {
recolor(mol.a8, crimson, 0.5);
orbit3(-34, 26, 7.9, 0.9);
show(loh, 0.4); show(soh, 0.4);
}
wait(0.5);
// leg 3 → the methyl at u 3.78, the tallest peak
par {
to(sw, value, 3.78, 1.93); to(nib, x, 3.78, 1.93);
shift(bar, (152, 0), 1.93);
orbit3(6, 18, 9.0, 1.8);
}
par {
recolor(mol.a5, cyan, 0.5); recolor(mol.a6, cyan, 0.5); recolor(mol.a7, cyan, 0.5);
orbit3(40, 24, 7.8, 0.9);
show(lch3, 0.4); show(sch3, 0.4);
}
wait(0.5);
// leg 4 → run out to δ 0, and pull back to see the whole molecule
par {
to(sw, value, 5, 1.71); to(nib, x, 5, 1.71);
shift(bar, (134, 0), 1.71);
orbit3(0, 18, 9.6, 1.7);
}
show(integ, 0.6);
wait(2.0);
// ═══ ACT 4: go back to the CH₂ peak, and go inside it ═══
//
// The pen runs back to the peak it started with, and then the scale changes underneath it: at 0–5
// ppm a 7 Hz splitting is 0.0175 ppm, two pixels. It was never one line.
// The pen rewinds, and `sw` rewinds with it: the readout counts back UP and the ink retracts,
// because the number on screen has to keep meaning the pen's position. Letting the pen travel while
// the readout sat at 0 Hz would break the one promise the scene makes.
par { fade(integ, 0.4); to(sw, value, 1.30, 1.1); to(nib, x, 1.30, 1.1); shift(bar, (-407, 0), 1.1); }
par { pulse(nib); orbit3(24, 20, 7.8, 1.0); }
wait(0.5);
par {
fade(trace, 0.5); fade(ax, 0.5); fade(axlab, 0.4); fade(bar, 0.4);
fade(nib, 0.4); fade(hread, 0.4); fade(dread, 0.4);
fade(lch3, 0.4); fade(sch3, 0.4); fade(loh, 0.4); fade(soh, 0.4);
fade(lch2, 0.4); fade(sch2, 0.4);
}
par { fade(t0, 0.3); fade(t1, 0.3); fade(t2, 0.3); fade(t3, 0.3); fade(t4, 0.3); fade(t5, 0.3); }
// ═══ ACT 5: the same pen, now measured in hertz ═══
par { show(zax, 0.6); show(zlab, 0.5); show(zwhy, 0.5); }
par { show(z1, 0.3); show(z2, 0.3); show(z3, 0.3); show(z4, 0.3); show(z5, 0.3); }
par { show(znib, 0.4); show(zread, 0.5); }
// the second sweep: 32 Hz, end to end, and the four lines arrive under the nib
par { to(zsw, value, 32, 3.4); to(znib, x, 32, 3.4); }
wait(0.4);
par { draw(jbar, 0.5); show(jlab, 0.5); }
par { pulse(jlab); orbit3(-18, 22, 8.0, 1.2); }
wait(1.8);
// ═══ ACT 6: why the unit matters ═══
par { fade(mlab, 0.4); show(k1, 0.6); }
show(k2, 0.5);
wait(0.7);
par { show(k3, 0.6); pulse(jlab); }
show(k4, 0.5);
par { orbit3(0, 16, 10.0, 2.4); }
wait(3.0);
mechanism-sn2
A mechanism, with the curly arrows aimed at real atoms. Hydroxide and bromoethane — the SN2
every organic course opens with. What makes it a mechanism rather than an equation is the two
curly arrows: one pair of electrons arriving to make a bond, one leaving with the bromide. The
thing to notice is in the source — arrow(att, nuc.a0, sub.a1, 62) aims at an ATOM and
arrow(go, sub.b1, sub.a2, 26) aims at a BOND, both resolving to wherever the structure kit
computed them. Change the bond unit from 90 to 120 and every arrow follows the molecule, because
an arrow anchored to a thing stays anchored to it. No new vocabulary: arrow already bowed into
an arc, it just needed to accept an entity where it used to insist on a coordinate.
// A mechanism, with the curly arrows aimed at real atoms
//
// Hydroxide and bromoethane: the SN2 substitution every organic course opens with. What makes it a
// mechanism rather than an equation is the two curly arrows — one showing a pair of electrons
// arriving to make a bond, one showing a pair leaving with the bromide.
//
// The thing worth noticing about the source is where the arrows are aimed. They are not drawn at
// coordinates worked out by hand:
//
// arrow(att, nuc.a0, sub.a1, 62); // from the hydroxide oxygen, to the carbon
// arrow(go, sub.b1, sub.a2, 26); // from the C-Br BOND, to the bromine
//
// `nuc.a0` is an atom, `sub.b1` is a bond, and both resolve to wherever the structure kit computed
// them. Change `unit` from 90 to 120 and every arrow follows the molecule, because an arrow anchored
// to a thing stays anchored to it. That is the whole reason this scene needs no new vocabulary:
// `arrow` already bows into an arc, and it now takes an entity where it used to insist on a point.
//
// A skeletal carbon is a bare vertex with no letter, so it used to have no entity to aim at — which
// is exactly the atom a mechanism arrow points to. Unlettered atoms now carry an invisible anchor
// for this, addressable as `{id}.a{i}` like any other.
title("SN2: where the electrons go");
canvas("16:9");
template("paper");
text(brand, (640, 30), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
// ── the reactants ──
//
// Bromoethane's atoms come out as a0 (the far carbon), a1 (the one bearing bromine) and a2 (the
// bromine); its bonds as b0 (C-C) and b1 (C-Br).
structure(sub, "CCBr", (400, 290), 90);
structure(nuc, "[OH-]", (210, 440), 90);
hidden(sub);
hidden(nuc);
// The hydroxide sits BELOW AND LEFT of the carbon while the bromine is above and right of it: the
// nucleophile comes in on the opposite side from the leaving group, which is what "backside attack"
// means and is not an accident of the layout.
text(nlab, (210, 492), "nucleophile");
size(nlab, 16); color(nlab, indigo); hidden(nlab);
text(llab, (584, 224), "leaving group");
size(llab, 16); color(llab, crimson); hidden(llab);
// ── the two arrows, anchored to atoms and bonds ──
arrow(att, nuc.a0, sub.a1, 62);
color(att, indigo);
stroke(att, 2.5);
untraced(att);
arrow(go, sub.b1, sub.a2, 26);
color(go, crimson);
stroke(go, 2.5);
untraced(go);
text(alab, (250, 356), "a pair arrives");
size(alab, 15); color(alab, indigo); hidden(alab);
text(glab, (566, 296), "a pair leaves");
size(glab, 15); color(glab, crimson); hidden(glab);
// ── the reaction arrow ──
arrow(rx, (612, 330), (740, 330));
color(rx, ink);
stroke(rx, 3);
untraced(rx);
// ── the products ──
structure(pro, "CCO", (900, 290), 90);
structure(lea, "[Br-]", (1122, 440), 90);
hidden(pro);
hidden(lea);
text(plab, (900, 200), "ethanol");
size(plab, 16); color(plab, dim); hidden(plab);
text(blab, (1122, 492), "bromide");
size(blab, 16); color(blab, dim); hidden(blab);
// ── the reading of it ──
text(sum, (640, 646), "one bond made, one bond broken — and the charge left with the bromide");
size(sum, 19); color(sum, ink); hidden(sum);
// ── ACT 1: the reactants, and which end is which ──
wait(0.4);
par { show(sub, 0.7); show(nuc, 0.7); }
par { show(nlab, 0.4); show(llab, 0.4); }
wait(0.7);
// ── ACT 2: the arrow that makes the bond ──
par { draw(att, 0.9); show(alab, 0.4); }
par { recolor(nuc.O, indigo, 0.5); pulse(nuc.O); }
wait(0.9);
// ── ACT 3: and the arrow that breaks one ──
par { draw(go, 0.7); show(glab, 0.4); }
par { recolor(sub.Br, crimson, 0.5); pulse(sub.Br); }
wait(1.1);
// ── ACT 4: what comes out ──
draw(rx, 0.6);
par { show(pro, 0.7); show(plab, 0.4); }
par { show(lea, 0.6); show(blab, 0.4); }
// the charge is on the bromide now, which is the bookkeeping the arrows were doing
recolor(lea.Br, crimson, 0.5);
pulse(lea.Br);
wait(0.8);
show(sum, 0.6);
wait(3.0);
equilibrium
Why “nothing is happening” is the wrong reading. Two views of one reversible reaction, side
by side, because the misconception lives in the gap between them: the concentrations flatten out
and stop moving, while the two RATES meet at a value that is equal and not zero. A still
picture of the left plot says the reaction stopped; the right plot says both directions are
still running at 0.225 mol dm⁻³ s⁻¹ and cancelling. That is dynamic equilibrium, and it is very
hard to say in a static diagram because the evidence for it is exactly what a flat line hides.
Then 0.50 M of A is added: the rates jump apart, the system relaxes, and it settles at a NEW
position with the SAME ratio — Le Chatelier as what the arithmetic does, not a rule to memorise.
Four fields hold the closed-form solution of d[A]/dt = -kf[A] + kr[B]; there is no new
vocabulary in the scene at all.
// Dynamic equilibrium — why "nothing is happening" is the wrong reading
//
// A reversible first-order reaction, A ⇌ B, with kf = 0.9 and kr = 0.3 per second. Two views of the
// same run, side by side, because the misconception lives in the gap between them:
//
// LEFT the concentrations, which flatten out and stop moving
// RIGHT the two rates, which meet — at a value that is EQUAL and NOT ZERO
//
// A still picture of the left-hand plot says "the reaction stopped". The right-hand plot says it did
// not: both directions are still running, at 0.225 mol dm⁻³ s⁻¹ each, and cancelling. That is the
// whole idea of dynamic equilibrium and it is very hard to say in a static diagram, because the
// evidence for it is precisely the thing a flat line hides.
//
// Then the second act: 0.50 M of A is added at t = 6 s. The rates jump apart, the system relaxes,
// and it settles at a NEW position with the SAME ratio — [B]/[A] = 3.00 either side. Le Chatelier is
// not a rule to memorise here; it is what the arithmetic does.
//
// NO NEW VOCABULARY. Four `field`s hold the closed-form solutions, `plot` draws them, and the
// choreography is `draw` / `show` / `pulse` from the core kit. Every number on screen comes out of
//
// [A](t) = A_eq + ([A]₀ − A_eq)·exp(−(kf + kr)·t)
//
// which is the exact solution of d[A]/dt = −kf[A] + kr[B] with [A] + [B] fixed. Nothing is placed by
// eye: change kf or kr and both plots, both equilibrium positions and the ratio all move together.
title("dynamic equilibrium: equal, not zero");
canvas("16:9");
template("paper");
text(brand, (640, 30), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
// ── the chemistry, as closed forms ──
//
// kf = 0.9, kr = 0.3, so K = kf/kr = 3 and the relaxation rate is kf + kr = 1.2 per second.
// Phase 1 starts from pure A at 1.00 M, so A_eq = 1.00 × kr/(kf+kr) = 0.25.
field(a1, "0.25 + 0.75*exp(-1.2*x)");
field(b1, "0.75 - 0.75*exp(-1.2*x)");
// Phase 2: 0.50 M of A added at t = 6, so the total is 1.50 M and A_eq = 1.50 × 0.25 = 0.375.
// [A] restarts from 0.75 (the 0.25 it had reached, plus the 0.50 added).
field(a2, "0.375 + 0.375*exp(-1.2*(x-6))");
field(b2, "1.125 - 0.375*exp(-1.2*(x-6))");
// ── LEFT: concentrations ──
coords(cc, (110, 600), (0, 14), (0, 1.25), 36, 300, 1);
hidden(cc);
// Explicit labels: the auto-numbering rounds to two significant figures, and a tick at 0.25 that
// prints "0.2" is worse than no tick at all — these are numbers the viewer is meant to read off.
ytick(cy1, cc, 0.25, "0.25");
ytick(cy2, cc, 0.75, "0.75");
ytick(cy3, cc, 1.125, "1.125");
for i in 1..4 { hidden(cy{i}); }
text(clab, (300, 208), "concentration / mol dm⁻³");
size(clab, 17); color(clab, dim); hidden(clab);
plot(ca1, (110, 600), 36, 300, "a1(x,0)", (0, 6));
plot(cb1, (110, 600), 36, 300, "b1(x,0)", (0, 6));
plot(ca2, (110, 600), 36, 300, "a2(x,0)", (6, 14));
plot(cb2, (110, 600), 36, 300, "b2(x,0)", (6, 14));
for i in 1..3 {
color(ca{i}, indigo); stroke(ca{i}, 3); untraced(ca{i});
color(cb{i}, crimson); stroke(cb{i}, 3); untraced(cb{i});
}
text(alab, (578, 512), "[A]");
size(alab, 19); color(alab, indigo); hidden(alab);
text(blab, (578, 252), "[B]");
size(blab, 19); color(blab, crimson); hidden(blab);
// ── RIGHT: the rates, which is where the misconception dies ──
//
// Written as k × concentration rather than pre-multiplied, so the source says what a rate IS.
coords(rc, (700, 600), (0, 14), (0, 0.75), 36, 440, 1);
hidden(rc);
ytick(ry, rc, 0.225, "0.225");
hidden(ry);
text(rlab, (900, 252), "rate / mol dm⁻³ s⁻¹");
size(rlab, 17); color(rlab, dim); hidden(rlab);
plot(rf1, (700, 600), 36, 440, "0.9*a1(x,0)", (0, 6));
plot(rr1, (700, 600), 36, 440, "0.3*b1(x,0)", (0, 6));
plot(rf2, (700, 600), 36, 440, "0.9*a2(x,0)", (6, 14));
plot(rr2, (700, 600), 36, 440, "0.3*b2(x,0)", (6, 14));
for i in 1..3 {
color(rf{i}, indigo); stroke(rf{i}, 3); untraced(rf{i});
color(rr{i}, crimson); stroke(rr{i}, 3); untraced(rr{i});
}
text(flab, (812, 322), "forward, kf[A]");
size(flab, 17); color(flab, indigo); hidden(flab);
text(vlab, (812, 566), "reverse, kr[B]");
size(vlab, 17); color(vlab, crimson); hidden(vlab);
// the point of the whole scene
dot(meet, (916, 501), 6);
color(meet, ink);
hidden(meet);
text(key, (1040, 470), "equal — and not zero");
size(key, 19); color(key, ink); hidden(key);
text(key2, (1078, 496), "both directions still running");
size(key2, 15); color(key2, dim); hidden(key2);
// ── the disturbance at t = 6 s ──
//
// [A] jumps instantly, so it is a vertical line rather than part of a curve. Endpoints are the two
// plots' own coordinates: t=6 is x = 110 + 6·36 = 326 on the left and 700 + 6·36 = 916 on the right.
line(jumpc, (326, 525), (326, 375));
color(jumpc, indigo);
stroke(jumpc, 2);
untraced(jumpc);
line(jumpr, (916, 501), (916, 303));
color(jumpr, indigo);
stroke(jumpr, 2);
untraced(jumpr);
text(add, (392, 356), "+0.50 M of A");
size(add, 16); color(add, indigo); hidden(add);
// ── and the reading of it ──
text(ratio, (640, 688), "[B]/[A] = 3.00 either side — the position moved, the ratio did not");
size(ratio, 18); color(ratio, ink); hidden(ratio);
// ── ACT 1: two empty axes ──
wait(0.4);
par { show(cc, 0.6); show(rc, 0.6); }
par { show(clab, 0.4); show(rlab, 0.4); }
par { show(cy1, 0.3); show(cy2, 0.3); show(cy3, 0.3); show(ry, 0.3); }
wait(0.5);
// ── ACT 2: the approach. Both views at once, because they are one run. ──
par {
draw(ca1, 2.6); draw(cb1, 2.6);
draw(rf1, 2.6); draw(rr1, 2.6);
}
par { show(alab, 0.4); show(blab, 0.4); show(flab, 0.4); show(vlab, 0.4); }
wait(0.7);
// ── ACT 3: the reading a flat line hides ──
par { show(meet, 0.4); pulse(meet); }
show(key, 0.5);
show(key2, 0.4);
wait(2.2);
// ── ACT 4: disturb it ──
par { fade(key, 0.4); fade(key2, 0.4); }
par { draw(jumpc, 0.4); draw(jumpr, 0.4); show(add, 0.4); }
wait(0.5);
// ── ACT 5: it settles somewhere new, at the same ratio ──
par {
draw(ca2, 2.4); draw(cb2, 2.4);
draw(rf2, 2.4); draw(rr2, 2.4);
}
wait(0.6);
show(ratio, 0.6);
wait(3.0);
titration
A titration where the curve is solved, not drawn. 25.0 mL of 0.100 M HCl against 0.100 M
NaOH, phenolphthalein. The shape every student memorises — flat, cliff, flat — except the cliff
arrives while you are watching the burette, which is the part a printed curve cannot say. The
pH is the exact solution of the charge balance [H+] - Kw/[H+] = (Ca·Va - Cb·Vb)/(Va + Vb), so
pH 7.00 at 25.0 mL falls out of the arithmetic instead of being placed; change a concentration
and the cliff moves on its own. And there is no new vocabulary in it: the apparatus is rect,
polygon and circle, the curve is plot over a field, and the choreography is draw / shift /
recolor / stagger — real chemistry teaching out of the core kit.
// A titration, with the curve solved rather than drawn
//
// 25.0 mL of 0.100 M hydrochloric acid, titrated with 0.100 M sodium hydroxide, phenolphthalein
// indicator. The shape every chemistry student is asked to memorise — flat, then a cliff, then flat
// again — and the point of animating it is that the cliff arrives *while you are watching the
// burette*, which is the part a printed curve cannot say.
//
// NOTHING here is a new builtin. The apparatus is rectangles and a polygon, the drops are circles,
// the curve is `plot`, and the choreography is `draw` / `shift` / `recolor` / `fade` from the core
// kit. That is the test this scene is meant to pass: real chemistry teaching out of vocabulary that
// already exists.
//
// The curve is not a drawn S-shape. It is the exact solution of the charge balance
//
// [H+] - Kw/[H+] = (Ca·Va - Cb·Vb) / (Va + Vb)
//
// rearranged to a quadratic and solved, so pH = 7.00 at 25.0 mL FALLS OUT of the arithmetic instead
// of being placed by hand. Change a concentration and the equivalence point moves on its own.
title("a titration, solved not drawn");
canvas("16:9");
template("paper");
text(brand, (640, 30), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
// ── the chemistry, as three reusable fields ──
//
// `field` inlines into any formula, so the same expression could feed a plot, a surface or a shader
// and provably be the same chemistry. Written in three steps because that is how the derivation
// reads, not because the engine needs it.
// excess strong acid (positive) or strong base (negative), diluted by the total volume
field(excess, "0.1*(25-x)/(25+x)");
// [H+] is the positive root of [H+]^2 - excess*[H+] - Kw = 0, with Kw = 1.0e-14. It is written
// TWICE, and the reason is arithmetic rather than chemistry: formulas evaluate in f32, and the two
// algebraically identical forms behave very differently there.
//
// acid side (excess > 0): (excess + sqrt(excess^2 + 4Kw)) / 2 — adds, so nothing cancels
// base side (excess < 0): 2Kw / (sqrt(excess^2 + 4Kw) - excess) — the conjugate form
//
// Use the first form past the equivalence point and it subtracts two nearly equal numbers: 4e-14 is
// eight orders below excess^2, vanishes in f32, and [H+] collapses to zero — log(0) is -inf and the
// whole upper branch silently disappears. The conjugate form divides instead of subtracting, so it
// holds. Checked against a f64 evaluation across 0-50 mL: both branches agree to 0.0000 pH, and
// both give exactly 7.000 at 25.0 mL, which is why they meet rather than merely nearly meet.
field(hacid, "(excess(x,0) + sqrt(excess(x,0)*excess(x,0) + 0.00000000000004))/2");
field(hbase, "0.00000000000002/(sqrt(excess(x,0)*excess(x,0) + 0.00000000000004) - excess(x,0))");
// ── the axes ──
coords(ax, (500, 610), (0, 50), (0, 14), 14, 28, 1, 5, 1);
hidden(ax);
// The axis names are placed by hand rather than passed to `coords`, which puts them at the axis
// END — on top of the arrow tip and the last tick numbers.
text(xname, (860, 668), "NaOH added / mL");
size(xname, 16); color(xname, dim); hidden(xname);
text(yname, (474, 196), "pH");
size(yname, 16); color(yname, dim); hidden(yname);
// the two halves of one curve, split at the equivalence point so the indicator can turn there
// pH = -log10[H+], and log10 is ln/ln(10)
plot(before, (500, 610), 14, 28, "-log(hacid(x,0))/2.302585", (0, 25));
plot(after, (500, 610), 14, 28, "-log(hbase(x,0))/2.302585", (25, 50));
color(before, ink);
color(after, ink);
stroke(before, 3);
stroke(after, 3);
untraced(before);
untraced(after);
// ── the apparatus, out of primitives ──
// the burette: a tube, its tap, and the tip the drops leave from
rect(tube, (180, 300), 26, 280);
outlined(tube);
outline(tube, dim);
stroke(tube, 2);
hidden(tube);
rect(titrant, (180, 300), 18, 272);
color(titrant, indigo);
opacity(titrant, 0.30);
hidden(titrant);
rect(tap, (180, 452), 44, 12);
color(tap, dim);
hidden(tap);
polygon(tip, (180, 464), (186, 472), (180, 486), (174, 472));
color(tip, dim);
hidden(tip);
// the flask, and what is in it
polygon(flask, (134, 642), (172, 556), (188, 556), (226, 642));
outlined(flask);
outline(flask, dim);
stroke(flask, 2);
hidden(flask);
// the solution: colourless while there is acid left, pink once there is not
polygon(soln, (140, 640), (167, 598), (193, 598), (220, 640));
color(soln, dim);
opacity(soln, 0.22);
hidden(soln);
text(caption, (196, 690), "0.100 M NaOH into 25.0 mL");
size(caption, 15);
color(caption, dim);
hidden(caption);
// four drops, reused by falling and fading. Declared up top because a constructor is build-time.
for i in 1..5 {
circle(d{i}, (180, 492), 4);
color(d{i}, indigo);
hidden(d{i});
}
// ── the equivalence point, revealed only after the curve has been through it ──
dot(eq, (850, 414), 6);
color(eq, crimson);
hidden(eq);
text(eqlab, (960, 392), "25.0 mL, pH 7.00");
size(eqlab, 18);
color(eqlab, crimson);
hidden(eqlab);
text(eqwhy, (1002, 418), "both branches solve to 7.00");
size(eqwhy, 15);
color(eqwhy, dim);
hidden(eqwhy);
// ── ACT 1: set the bench up ──
wait(0.4);
par { show(tube, 0.5); show(tap, 0.5); show(tip, 0.4); }
par { show(titrant, 0.5); show(flask, 0.5); show(soln, 0.5); }
par { show(ax, 0.7); show(xname, 0.5); show(yname, 0.5); show(caption, 0.5); }
wait(0.6);
// ── ACT 2: the flat part. Drops fall, and almost nothing happens to the pH. ──
//
// This is the half of a titration that surprises people: a quarter of the base is in and the pH has
// moved by less than one unit, because a strong acid buffers itself by sheer excess.
par {
draw(before, 3.4);
stagger(0.55) {
par { show(d1, 0.1); shift(d1, (0, 64), 0.5); fade(d1, 0.15); }
par { show(d2, 0.1); shift(d2, (0, 64), 0.5); fade(d2, 0.15); }
par { show(d3, 0.1); shift(d3, (0, 64), 0.5); fade(d3, 0.15); }
par { show(d4, 0.1); shift(d4, (0, 64), 0.5); fade(d4, 0.15); }
}
}
// ── ACT 3: the endpoint. One drop, and the indicator turns. ──
par { recolor(soln, crimson, 0.45); pulse(soln); }
par { show(eq, 0.4); show(eqlab, 0.4); }
wait(0.9);
show(eqwhy, 0.5);
wait(1.0);
// ── ACT 4: past it, and flat again ──
draw(after, 2.6);
wait(2.6);
reaction-esterification
A + B → C, the way a textbook draws it — and there is no reaction builtin behind it.
Acetic acid and ethanol give ethyl acetate and water; every molecule is a SMILES string laid out
by the chemistry kit, and the plus signs, the arrow, the conditions and the whole choreography
are the core kit. What the kit contributes is that the structures are REAL: the geometry is
computed from the graph, so the ester bond in the product is the same bond that formed. The
teaching beat is the middle one — the acid’s -OH and the alcohol’s -H are marked BEFORE the
arrow, and the same colour turns up in the water afterwards, so “where did the water come from?”
is answered by watching rather than by being told. SMILES atoms are numbered in written order,
which is why acid.a3 is the hydroxyl oxygen that leaves.
// A + B → C, the way a textbook draws it
//
// Esterification: acetic acid and ethanol give ethyl acetate and water. The classic school
// reaction, and the classic school diagram — reactants, a plus, an arrow with its conditions,
// products.
//
// There is no reaction builtin in Manic and this scene does not need one. Every molecule is a
// SMILES string laid out by the chemistry kit; the plus signs, the arrow, the conditions and the
// whole choreography are the core kit. What the kit contributes is that the structures are real:
// the geometry is computed from the graph, not drawn by hand, so the ester bond in the product is
// the same bond that formed.
//
// The teaching beat is the middle one. The acid's -OH and the alcohol's -H are marked BEFORE the
// arrow, and the same colour turns up in the water afterwards — so the question "where did the
// water come from?" is answered by watching, not by being told.
title("esterification");
canvas("16:9");
template("paper");
text(brand, (640, 40), "maniclang.com");
display(brand);
size(brand, 18);
color(brand, dim);
text(heading, (640, 108), "acid + alcohol gives an ester, and water");
size(heading, 30);
color(heading, ink);
hidden(heading);
// ── the cast, all from SMILES strings ──
structure(acid, "CC(=O)O", (190, 348), 76);
structure(alc, "CCO", (487, 348), 76);
structure(ester, "CC(=O)OCC", (900, 348), 66);
structure(water, "O", (1198, 348), 60);
// Everything starts as bare geometry, drawn on beat by beat.
untraced(acid.bonds);
untraced(alc.bonds);
untraced(ester.bonds);
hidden(acid.labels);
hidden(alc.labels);
hidden(ester.labels);
hidden(water);
// names under each structure
text(n1, (190, 500), "acetic acid");
text(n2, (487, 500), "ethanol");
text(n3, (900, 500), "ethyl acetate");
text(n4, (1198, 500), "water");
for i in 1..5 {
size(n{i}, 21);
color(n{i}, dim);
hidden(n{i});
}
// the arithmetic of the diagram
text(plus1, (348, 352), "+");
text(plus2, (1105, 352), "+");
size(plus1, 34); size(plus2, 34);
color(plus1, ink); color(plus2, ink);
hidden(plus1); hidden(plus2);
// the arrow, and what sits over it
arrow(rx, (640, 352), (782, 352));
color(rx, ink);
stroke(rx, 2.6);
untraced(rx);
text(cond, (711, 312), "H2SO4");
size(cond, 19);
color(cond, crimson);
hidden(cond);
text(cond2, (711, 390), "heat");
size(cond2, 19);
color(cond2, dim);
hidden(cond2);
// what the marked atoms become — declared up front, revealed later
text(note, (640, 600), "the acid gives up an OH, the alcohol an H — together, water");
size(note, 22);
color(note, crimson);
hidden(note);
// ── 1. the reactants, written on ──
wait(0.5);
show(heading, 0.6);
wait(0.3);
stagger(0.09) { draw(acid.bonds, 0.45); }
par { show(acid.labels, 0.5); show(n1, 0.4); }
wait(0.35);
show(plus1, 0.3);
stagger(0.09) { draw(alc.bonds, 0.45); }
par { show(alc.labels, 0.5); show(n2, 0.4); }
wait(1.0);
// ── 2. mark what is about to leave ──
//
// SMILES atoms are numbered in the order they are written, so `CC(=O)O` puts its hydroxyl oxygen
// last — a3 — and `CCO` puts its oxygen at a2. That is the -OH and the -H that become water.
par {
recolor(acid.a3, crimson, 0.6);
recolor(alc.a2, crimson, 0.6);
}
par { pulse(acid.a3); pulse(alc.a2); }
wait(1.2);
// ── 3. the arrow ──
par {
draw(rx, 0.7);
show(cond, 0.5);
show(cond2, 0.5);
}
wait(0.8);
// ── 4. the product, and the water it leaves behind ──
stagger(0.07) { draw(ester.bonds, 0.4); }
par { show(ester.labels, 0.5); show(n3, 0.4); }
wait(0.5);
show(plus2, 0.3);
par { show(water, 0.6); show(n4, 0.4); }
// the water carries the colour of the atoms it was made from
recolor(water.O, crimson, 0.7);
par { pulse(water.O); show(note, 0.6); }
wait(2.6);
// ── 5. and the bond that was made ──
framebox(made, ester.O, 16, crimson);
hidden(made);
text(bond, (640, 600), "and the bond that was made joins them");
size(bond, 21);
color(bond, dim);
hidden(bond);
par {
fade(note, 0.4);
show(made, 0.6);
show(bond, 0.5);
}
wait(2.8);
reaction-ord-screen
One real experiment, from the record. Open Reaction Database ord-00550a5de34040cea861e1ce0aca6f9e: a C-N coupling run in a single well of a 1536-well plate. Five solutions assemble across the top in the order the robot added them, each drawn on from the record’s own SMILES, with the record’s own amounts; then the conditions; then the outcome. The scheme is never taken away, so the last frame holds the whole experiment at once — which is the only honest way to show a result that depends on all of it. And the result is the point: 0.000% of the product they were aiming at, 5.201% of the other isomer. The reaction went, just not where they aimed it. The apparatus is RIGGED rather than slid — each instrument is split across two SVG files so its moving part is its own entity, and the head dips, the foil seals, the mixer block shakes and its indicator heats, all on core verbs.
// A C–N coupling, built the way the record reads it
//
// Open Reaction Database ord-00550a5de34040cea861e1ce0aca6f9e — Cernak lab, Michigan,
// doi 10.1021/jacs.6c05959. Sample XZ_01-115-60_3_K3.
//
// The scheme ASSEMBLES and then stays: five solutions across the top in the order the robot added
// them, the arrow and its conditions, then the outcome. Nothing is taken away, so by the last frame
// the whole experiment is on screen at once — which is how the record itself is laid out, and the
// only honest way to show a reaction whose answer depends on all of it.
//
// Every structure is drawn on from the record's own SMILES. Every number is the record's own,
// including the two that matter: 0.000% of the product they wanted, and 5.201% of the isomer they
// did not.
title("a C-N coupling, one well of 1536");
canvas("16:9");
template("paper");
text(brand, (640, 30), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
// ── the apparatus, RIGGED: parts that move are their own entities ──
//
// Each instrument is split across files so that its moving part is a separate manic entity.
// A machine imported as one file can only be shifted as a blob, which is motion that ignores the
// chemistry; imported as parts, the head travels while the frame holds still and the mixer block
// shakes while its feet do not. Nothing here needs a new builtin — `shift`, `recolor`, `shake` and
// `pulse` are the core kit, and they work because `svg()` emits a native entity per subpath rather
// than a texture.
//
// The rail is a `rect`, not artwork: it is a straight line whose length has to match the row it
// serves, so a primitive is both simpler and parametric.
rect(rail, (574, 43), 1012, 4);
color(rail, dim);
opacity(rail, 0.4);
hidden(rail);
// The head hangs off the rail. Placement is arithmetic, not eyeballing: the two files share one
// coordinate system, so with the head at 62px wide (scale 62/144 = 0.43) its centre sits
// (74 - 127) * 0.43 = 23px below the rail, and the tips reach 30px below that. Rest is 66, so
// the tips sit at 96 and a 10px dip reaches 106 — still clear of the role labels at 118.
svg(hd, (150, 66), "asset:svg/chem/liquid-handler-head.svg", 62);
hidden(hd);
// the plate the additions go into, off at the end of the rail
svg(plate, (1180, 132), "asset:svg/chem/plate-1536.svg", 170);
hidden(plate);
// the foil seal, waiting off-frame to the right — a 0.2 microlitre well does not survive
// eighteen hours at 60 C unsealed
svg(foil, (1292, 132), "asset:svg/chem/foil-seal.svg", 170);
hidden(foil);
// The mixer, in the empty quarter under the arrow. Same trick: body and block are separate files,
// so `shake` moves the block alone.
svg(mxBody, (280, 582), "asset:svg/chem/thermomixer-body.svg", 200);
hidden(mxBody);
svg(mxBlock, (280, 535), "asset:svg/chem/thermomixer-block.svg", 165);
hidden(mxBlock);
// the heat indicator changes over time, so it is a primitive rather than baked artwork
circle(led, (240, 577), 5);
color(led, dim);
hidden(led);
// ── the five solutions, left to right, in addition order ──
//
// Each cell is the reagent and the DMSO it arrived in, because that is what went into the well.
structure(a1, "C[Si](C)(C)[O-].[Na+]", (108, 196), 30);
// The ligand comes from a 2-D depiction FILE, not its SMILES — and the reason is worth knowing.
// The layout grows ALONG the string, so the order the ring closures are written in matters: the
// record's own `COC1=CC=NC2=C3N=CC=C(OC)C3=CC=C12` strands a bond 3.6 lengths long and is refused,
// while PubChem's canonical form of the SAME molecule draws cleanly. Rather than quietly swap in a
// different string than the record's, use the depiction — which is what the refusal points at.
// Everything else in the scene is the record's SMILES, verbatim.
structure(a2, "asset:molecules/dimethoxyphenanthroline-2d.sdf", (330, 200), 21);
structure(a3, "[Cu]O[Cu]", (556, 196), 34);
structure(a4, "C1=CC=C(C2CCNCC2)C=C1", (760, 196), 28);
structure(a5, "IC1=CC=CN=C1", (960, 196), 32);
structure(d1, "CS(C)=O", (196, 196), 22);
structure(d2, "CS(C)=O", (430, 196), 22);
structure(d3, "CS(C)=O", (640, 196), 22);
structure(d4, "CS(C)=O", (856, 196), 22);
structure(d5, "CS(C)=O", (1044, 196), 22);
// Written out rather than looped: `a{i}.bonds` does not interpolate — a loop index reaches an id
// but not a dotted tag on it, which is a known gap logged in CAPABILITIES.
untraced(a1.bonds); untraced(a2.bonds); untraced(a3.bonds); untraced(a4.bonds); untraced(a5.bonds);
untraced(d1.bonds); untraced(d2.bonds); untraced(d3.bonds); untraced(d4.bonds); untraced(d5.bonds);
hidden(a1.labels); hidden(a2.labels); hidden(a3.labels); hidden(a4.labels); hidden(a5.labels);
hidden(d1.labels); hidden(d2.labels); hidden(d3.labels); hidden(d4.labels); hidden(d5.labels);
// role, name, amount — kept to three short lines per cell
text(r1, (150, 118), "base");
text(r2, (378, 118), "ligand");
text(r3, (598, 118), "catalyst");
text(r4, (806, 118), "nucleophile");
text(r5, (1004, 118), "electrophile");
for i in 1..6 { size(r{i}, 19); color(r{i}, ink); hidden(r{i}); }
text(v1, (150, 286), "0.08 umol");
text(v2, (378, 286), "0.004 umol");
text(v3, (598, 286), "0.004 umol");
text(v4, (806, 286), "0.06 umol");
text(v5, (1004, 286), "0.04 umol");
for i in 1..6 { size(v{i}, 17); color(v{i}, crimson); hidden(v{i}); }
// the addition-order strip, which is the whole point of showing them in a row
text(o1, (150, 318), "1");
text(o2, (378, 318), "2");
text(o3, (598, 318), "3");
text(o4, (806, 318), "4");
text(o5, (1004, 318), "5");
for i in 1..6 { size(o{i}, 22); color(o{i}, dim); hidden(o{i}); }
rect(strip, (577, 318), 1010, 34);
color(strip, dim);
outlined(strip);
stroke(strip, 1.2);
opacity(strip, 0.35);
hidden(strip);
// ── the arrow, and what happens over it ──
arrow(rx, (240, 470), (470, 470));
color(rx, ink);
stroke(rx, 3);
untraced(rx);
text(c1, (355, 418), "60 °C · dry nitrogen");
text(c2, (355, 444), "800 rpm · 18 h");
for i in 1..3 { size(c{i}, 19); color(c{i}, ink); hidden(c{i}); }
// ── the outcome ──
structure(p1, "C1(N2CCC(C3=CC=CC=C3)CC2)=CC=CN=C1", (612, 500), 30);
structure(p2, "Cn1c(=O)c2c(ncn2C)n(C)c1=O", (826, 500), 28);
structure(p3, "C1(N2CCC(C3=CC=CC=C3)CC2)=CC=NC=C1", (1010, 500), 30);
untraced(p1.bonds); untraced(p2.bonds); untraced(p3.bonds);
hidden(p1.labels); hidden(p2.labels); hidden(p3.labels);
text(y1, (612, 620), "0.000%");
size(y1, 30); color(y1, crimson); hidden(y1);
text(y2, (826, 620), "standard");
size(y2, 20); color(y2, dim); hidden(y2);
text(y3, (1010, 620), "5.201%");
size(y3, 30); color(y3, indigo); hidden(y3);
text(y1b, (612, 654), "the target");
size(y1b, 17); color(y1b, dim); hidden(y1b);
text(y3b, (1010, 654), "the other isomer");
size(y3b, 17); color(y3b, dim); hidden(y3b);
text(cite, (640, 700), "ORD ord-00550a5de34040cea861e1ce0aca6f9e · doi 10.1021/jacs.6c05959");
size(cite, 13); color(cite, dim); hidden(cite);
// ── ACT 1: where this happens ──
wait(0.4);
par { show(plate, 0.7); show(cite, 0.5); }
par { show(rail, 0.5); show(hd, 0.6); show(strip, 0.5); }
// the head is charged, and stays charged: the tips are `hd.p3`..`hd.p6`, four of the seven
// subpaths in the head file, addressable because an imported SVG is entities and not a picture
par {
recolor(hd.p3, indigo, 0.4);
recolor(hd.p4, indigo, 0.4);
recolor(hd.p5, indigo, 0.4);
recolor(hd.p6, indigo, 0.4);
}
wait(0.6);
// ── ACT 2: five additions. The head DIPS at each one; each cell draws on and STAYS. ──
//
// The dip is the beat: the head goes down as the reagent goes in, so the machine is doing the
// thing the addition-order strip is counting, rather than sliding past it.
par { show(r1, 0.3); show(o1, 0.3); }
par { shift(hd, (0, 10), 0.25); pulse(hd); }
par { draw(a1.bonds, 0.7); draw(d1.bonds, 0.5); }
par { show(a1.labels, 0.4); show(d1.labels, 0.4); show(v1, 0.4); }
shift(hd, (0, -10), 0.25);
par { shift(hd, (228, 0), 0.5); show(r2, 0.3); show(o2, 0.3); }
par { shift(hd, (0, 10), 0.25); pulse(hd); }
par { draw(a2.bonds, 0.9); draw(d2.bonds, 0.5); }
par { show(a2.labels, 0.4); show(d2.labels, 0.4); show(v2, 0.4); }
shift(hd, (0, -10), 0.25);
par { shift(hd, (220, 0), 0.5); show(r3, 0.3); show(o3, 0.3); }
par { shift(hd, (0, 10), 0.25); pulse(hd); }
par { draw(a3.bonds, 0.6); draw(d3.bonds, 0.5); }
par { show(a3.labels, 0.4); show(d3.labels, 0.4); show(v3, 0.4); }
shift(hd, (0, -10), 0.25);
par { shift(hd, (208, 0), 0.5); show(r4, 0.3); show(o4, 0.3); }
par { shift(hd, (0, 10), 0.25); pulse(hd); }
par { draw(a4.bonds, 0.9); draw(d4.bonds, 0.5); }
par { show(a4.labels, 0.4); show(d4.labels, 0.4); show(v4, 0.4); }
shift(hd, (0, -10), 0.25);
par { shift(hd, (198, 0), 0.5); show(r5, 0.3); show(o5, 0.3); }
par { shift(hd, (0, 10), 0.25); pulse(hd); }
par { draw(a5.bonds, 0.7); draw(d5.bonds, 0.5); }
par { show(a5.labels, 0.4); show(d5.labels, 0.4); show(v5, 0.4); }
shift(hd, (0, -10), 0.25);
// the limiting reagent, marked where it stands
par { recolor(v5, indigo, 0.5); pulse(a5.I); }
wait(0.8);
// ── ACT 3: seal it, heat it, shake it. ──
//
// Three separate motions on three separate parts, which is the whole reason the instruments were
// split into files: the foil travels, the block shakes, the indicator changes colour.
par { fade(hd, 0.4); fade(rail, 0.4); }
par { show(foil, 0.3); shift(foil, (-112, 0), 0.7); }
wait(0.2);
par { show(mxBody, 0.5); show(mxBlock, 0.5); show(led, 0.4); }
par { draw(rx, 0.8); show(c1, 0.4); show(c2, 0.4); }
recolor(led, crimson, 0.5); // 60 C, and the block starts to move
shake(mxBlock, 0.5);
shake(mxBlock, 0.5);
shake(mxBlock, 0.5);
shake(mxBlock, 0.5);
wait(0.7);
// ── ACT 4: the outcome — A + B → C, and what actually came out ──
// the bench has done its job; the chemistry is what is left
par { fade(mxBody, 0.5); fade(mxBlock, 0.5); fade(led, 0.4); }
par { draw(p2.bonds, 0.7); show(p2.labels, 0.4); }
show(y2, 0.4);
wait(0.5);
par { draw(p1.bonds, 0.9); show(p1.labels, 0.4); }
par { show(y1, 0.5); show(y1b, 0.4); }
wait(1.0);
par { draw(p3.bonds, 0.9); show(p3.labels, 0.4); }
par { show(y3, 0.5); show(y3b, 0.4); }
wait(1.2);
// the one difference between them, marked on both
par { recolor(p1.N, crimson, 0.6); recolor(p3.N, indigo, 0.6); }
par { pulse(p1.N); pulse(p3.N); }
wait(1.0);
// No closing line. The two marked nitrogens and the two numbers under them are the reading of it,
// and saying it in words as well only tells the viewer what they have just been shown.
wait(3.4);
structure-to-shape
The step students actually get stuck on. A structural formula is a page of lines and
letters; a molecule is a lump in three dimensions, and told they are the same thing most
people nod and privately keep two unrelated pictures. So the formula writes itself on bond by
bond (untraced + stagger { draw(...) } — core verbs, nothing chemistry-specific), then the
same compound’s 3-D conformer appears beside it, turning. Then recolor marks the four
nitrogens on BOTH sides at once, because each kit tags an atom by its element — so “the
nitrogens” is something you can say to the scene rather than a set of indices to look up.
The left side is read from the 2-D depiction record, the right from the 3-D conformer.
// The flat formula and the real shape are the same molecule
//
// This is the step students actually get stuck on. A structural formula is a page of lines and
// letters; a molecule is a lump in three dimensions. Told they are the same thing, most people
// nod and privately keep two unrelated pictures.
//
// So: draw the formula on, bond by bond, the way you would on paper. Then put the real geometry
// beside it, turning, from the same compound in PubChem. Both sides are read from files — the
// left from the 2-D depiction record, the right from the 3-D conformer — so nothing here is an
// artist's impression of the other.
title("the formula and the shape");
canvas("16:9");
template("paper");
text(brand, (640, 42), "maniclang.com");
display(brand);
size(brand, 19);
color(brand, dim);
// The 3-D scene covers the whole canvas, so the molecule is put at the ORIGIN and the camera is
// aimed off to the left instead — which slides the subject into the right half of the frame and
// keeps it there while it turns. Moving the molecule sideways instead pushes it toward the camera
// and it grows and clips.
camera3((0, -18, 5), (-4.2, 0, 0), 32);
// ── the page: a structural formula, drawn by hand ──
structure(flat, "asset:molecules/caffeine-2d.sdf", (350, 372), 54);
untraced(flat.bonds);
hidden(flat.labels);
// ── the substance: the same compound's 3-D conformer ──
molecule3(solid, "asset:molecules/caffeine.sdf", (0, 0, 0), 0.66, "spin=22");
hidden(solid);
text(t1, (350, 128), "caffeine, as it is written");
size(t1, 26); color(t1, ink); hidden(t1);
text(t2, (350, 660), "one line per bond, letters where a chemist letters them");
size(t2, 19); color(t2, dim); hidden(t2);
text(t3, (966, 128), "caffeine, as it is");
size(t3, 26); color(t3, ink); hidden(t3);
text(t4, (966, 660), "the same compound, its computed 3-D conformer");
size(t4, 19); color(t4, dim); hidden(t4);
// ── beat 1: write the formula ──
wait(0.6);
show(t1, 0.5);
// Drawn bond by bond, in the file's own order — the handwriting effect, from the core kit's
// `stagger` and `draw`. Nothing chemistry-specific about it.
//
// Addressed by TAG, not by index: a skeletal drawing skips every C–H bond, so the surviving bond
// ids are the file's own numbering with gaps in it. `{id}.bonds` is whatever was actually drawn.
stagger(0.05) {
draw(flat.bonds, 0.4);
}
show(flat.labels, 0.6);
show(t2, 0.5);
wait(1.4);
// ── beat 2: and here is the thing itself ──
par { show(solid, 0.9); show(t3, 0.5); }
show(t4, 0.5);
wait(2.0);
// ── beat 3: the same nitrogens, on both sides at once ──
//
// Four nitrogens on the page and four in the solid. Recolouring them is one verb per side, because
// both kits tag an atom by its element — so "the nitrogens" is something you can say to the
// scene, not a set of indices you have to look up.
text(t5, (640, 596), "four nitrogens, the same four");
size(t5, 23); color(t5, indigo); hidden(t5);
// `recolor`, not `color`: `color` is a build-time setting and belongs above the timeline, while
// `recolor` is the verb that changes a colour DURING the story.
par {
recolor(flat.N, indigo, 0.7);
recolor(solid.N, indigo, 0.7);
show(t5, 0.5);
}
wait(2.6);
// ── beat 4: and the oxygens ──
text(t6, (640, 596), "and the two oxygens");
size(t6, 23); color(t6, red); hidden(t6);
par { fade(t5, 0.35); }
par {
pulse(flat.O);
pulse(solid.O);
show(t6, 0.5);
}
wait(2.8);
molecule-shapes
Why molecules have shapes — water bent, ammonia pyramidal, methane tetrahedral, carbon dioxide linear, each turning on its own axis. The labels are shape CLASSES, not measurements: the geometry is the file’s and there is no number in the script to disagree with it. Then caffeine four ways — ball & stick, sticks, hydrogens hidden (the skeleton a chemist draws), and space-filling.
// Why molecules have shapes
//
// Four molecules, four shapes — and not one coordinate in this file. Each `molecule3` reads a
// PubChem structure file (a real "Download → SDF"), so every bond angle you see is the one in the
// conformer someone computed: water bent, ammonia pyramidal, methane tetrahedral, carbon dioxide
// dead straight.
//
// They turn on their own axes because that is the default — and it is the whole argument for doing
// this in three dimensions. On a flat page methane is four lines from a dot and "tetrahedral" is a
// claim you take on trust; turning, the shape is simply visible. Same for ammonia's pyramid, which
// flat looks like a T.
title("why molecules have shapes");
canvas("16:9");
template("black");
text(brand, (640, 44), "maniclang.com");
display(brand);
size(brand, 20);
color(brand, dim);
// Looking down at about 30°, not along the plane: caffeine is FLAT, and edge-on a flat molecule
// is a line. The elevation also happens to be what makes methane's tetrahedron read.
camera3((0, -15, 8.5), (0, 0, 0), 40);
// Four PubChem downloads, laid out left to right at one magnification, so they are genuinely
// comparable in size. `spin` is degrees per second — the default; `spin=0` would hold one still.
molecule3(waterm, "asset:molecules/water.sdf", (-6.4, 0, 0), 1.5, "spin=26");
molecule3(ammonia, "asset:molecules/ammonia.sdf", (-2.1, 0, 0), 1.5, "spin=26");
molecule3(methane, "asset:molecules/methane.sdf", (2.2, 0, 0), 1.5, "spin=26");
molecule3(co2, "asset:molecules/carbon-dioxide.sdf", (6.6, 0, 0), 1.5, "spin=26");
hidden(waterm);
hidden(ammonia);
hidden(methane);
hidden(co2);
// Formulae and shape CLASSES — not measurements. The geometry on screen is the file's, and nothing
// here restates it as a number.
text(t1, (196, 560), "H2O");
text(s1, (196, 600), "bent");
text(t2, (452, 560), "NH3");
text(s2, (452, 600), "pyramidal");
text(t3, (760, 560), "CH4");
text(s3, (760, 600), "tetrahedral");
text(t4, (1060, 560), "CO2");
text(s4, (1060, 600), "linear");
for i in 1..5 {
hidden(t{i});
hidden(s{i});
size(t{i}, 30);
size(s{i}, 22);
color(s{i}, dim);
}
color(t1, red);
color(t2, blue);
color(t3, silver);
color(t4, red);
// ── one at a time, each already turning as it arrives ──
wait(0.7);
par { show(waterm, 0.7); show(t1, 0.5); show(s1, 0.5); }
wait(0.9);
par { show(ammonia, 0.7); show(t2, 0.5); show(s2, 0.5); }
wait(0.9);
par { show(methane, 0.7); show(t3, 0.5); show(s3, 0.5); }
wait(0.9);
par { show(co2, 0.7); show(t4, 0.5); show(s4, 0.5); }
wait(1.6);
// ── the four styles a chemistry viewer offers, on the one molecule ──
//
// Same file, same coordinates, four questions. Ball-and-stick shows what is bonded to what;
// sticks gets the spheres out of the way of the geometry; hiding the hydrogens leaves the carbon
// skeleton an organic chemist actually draws; space-filling shows the shape the molecule presents
// to the world — which is the one that explains why it fits somewhere, or does not.
par {
fade(waterm, 0.5); fade(ammonia, 0.5); fade(methane, 0.5); fade(co2, 0.5);
fade(t1, 0.4); fade(s1, 0.4); fade(t2, 0.4); fade(s2, 0.4);
fade(t3, 0.4); fade(s3, 0.4); fade(t4, 0.4); fade(s4, 0.4);
}
// No `tilt` here on purpose. Caffeine is FLAT and lies in the xy plane, so turning it about the
// vertical keeps it in its own plane and it presents the same face to the camera throughout. Lean
// it with `tilt` and the spin walks it through edge-on, where a flat molecule is a line — `tilt`
// and `axis=x|y|z` are there for when you want that (a molecule rolling end over end), not for a
// comparison like this one.
molecule3(c1, "asset:molecules/caffeine.sdf", (-8.4, 0, 0), 0.52, "style=ball");
molecule3(c2, "asset:molecules/caffeine.sdf", (-2.8, 0, 0), 0.52, "style=sticks");
molecule3(c3, "asset:molecules/caffeine.sdf", (2.8, 0, 0), 0.52, "style=sticks hydrogens=0");
molecule3(c4, "asset:molecules/caffeine.sdf", (8.4, 0, 0), 0.52, "style=spacefill");
hidden(c1); hidden(c2); hidden(c3); hidden(c4);
text(cap, (640, 130), "caffeine, four ways to look at it");
hidden(cap);
size(cap, 28);
color(cap, cyan);
text(k1, (222, 600), "ball & stick");
text(k2, (492, 600), "sticks");
text(k3, (770, 600), "no hydrogens");
text(k4, (1052, 600), "space-filling");
for i in 1..5 {
hidden(k{i});
size(k{i}, 21);
color(k{i}, dim);
}
show(cap, 0.5);
par { show(c1, 0.6); show(k1, 0.4); }
wait(0.7);
par { show(c2, 0.6); show(k2, 0.4); }
wait(0.7);
par { show(c3, 0.6); show(k3, 0.4); }
wait(0.7);
par { show(c4, 0.6); show(k4, 0.4); }
wait(3.0);
Circuits — schematics that work
A netlist in, a working schematic out: Modified Nodal Analysis under easy builtins, so every voltage, current and glow on screen was solved rather than drawn. The schematic draws itself one component at a time and each PREFIX of it is solved, so nothing flows until the loop closes; cut takes a component out of the circuit and what is left is re-solved from scratch. See the circuits guide.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
circuit-broken-filament
NCERT Class 9, Fig. 3.17, Q3 — if the filament of one of the lamps is broken, will
the other glow? Two lamps in series share one current, and each lamp’s halo carries
P = V·I, so when cut breaks one filament BOTH go dark and the meter falls to 0.00 mA on
its own. The answer is nowhere in the file. cut decides the physics and the scene decides
the theatre: since the question is about a broken filament rather than a lamp removed from
its holder, the symbol comes back with show and a slash marks the break.
// Class 9 · NCERT Science, Fig. 3.17, question 3
//
// "If the filament of one of the lamps is broken, will the other glow?
// Justify your answer."
//
// The answer is not asserted anywhere in this file. Two lamps sit in series across a cell; the
// solver gives them one shared current, and each lamp's halo carries P = V·I on its opacity, so
// they light because they are dissipating power. `cut` takes one filament out of the circuit —
// the circuit that is left is re-analysed and re-solved from scratch — and both lamps go dark
// because both are now dissipating nothing. The meter falls to zero on its own.
//
// A series circuit is one path. Break it anywhere and you have broken it everywhere.
title("if one filament breaks");
canvas("16:9");
template("paper");
text(brand, (640, 40), "maniclang.com");
display(brand);
size(brand, 20);
color(brand, dim);
// ── the schematic, drawn the way the book draws it: two lamps, one cell, one loop ──
//
// And no ground symbol, exactly as the book draws it: a ground only NAMES the reference node, and
// this loop has nothing to reference. The kit elects the cell's negative terminal.
circuit(fig, (560, 330), `
lamp 1 0 5 0 r=120 name=L1
lamp 5 0 9 0 r=120 name=L2
wire 9 0 9 4
wire 9 4 6 4
dc-voltage 6 4 4 4 v=6
wire 4 4 1 4
wire 1 4 1 0
`, 62, 0, 0);
current(fig, 1.4, circle, crimson, 4);
probe(fig, L1, (-14, -52));
// The halo behind a lamp is the lamp's own colour by default; on a cream page a warm one reads as
// light. It is an ordinary entity tagged {id}.glow, so a core verb restyles it.
color(fig.glow, orange);
// The object beside the symbol, because a beginner meeting ⊗ for the first time has no reason
// to know it means a lamp. Vector art, so it draws and fades like anything else.
svg(bulb, (1090, 250), "assets/svg/heroicons/light-bulb.svg", 118);
color(bulb, gold);
hidden(bulb);
text(l1, (1090, 340), "the lamp");
hidden(l1);
size(l1, 22);
color(l1, dim);
// ── the question, and then the answer ──
text(q, (640, 120), "one filament breaks — does the other still glow?");
hidden(q);
size(q, 30);
color(q, ink);
// The break itself. `cut` decides the PHYSICS; the theatre is the author's, and this question is
// about a broken filament rather than a lamp taken out of its holder — so the symbol stays on the
// page and a slash marks the break. The lamp is still there. It just cannot carry anything.
// L1's symbol sits at (436, 206): a circuit is placed by the CENTRE of its bounding box, so a
// grid point lands at `origin + p × unit` where `origin = centre − span/2 − lo × unit`. Here that
// is (250, 206), and L1 spans grid x 1→5, so its middle is x = 250 + 3 × 62.
line(break1, (418, 188), (454, 224));
hidden(break1);
color(break1, crimson);
stroke(break1, 3);
text(a1, (1075, 470), "one path,");
hidden(a1);
size(a1, 26);
color(a1, crimson);
text(a2, (1075, 508), "one current");
hidden(a2);
size(a2, 26);
color(a2, crimson);
text(a3, (1075, 560), "break it anywhere");
hidden(a3);
size(a3, 26);
color(a3, ink);
text(a4, (1075, 598), "and it is broken");
hidden(a4);
size(a4, 26);
color(a4, ink);
text(a5, (1075, 636), "everywhere");
hidden(a5);
size(a5, 26);
color(a5, ink);
// ── the beats ──
show(q, 0.6);
wait(0.3);
// the loop closes and both lamps come up together: one current, so one brightness
run(fig, 6.0);
par {
show(bulb, 0.7);
show(l1, 0.5);
}
wait(0.6);
par {
show(a1, 0.4);
show(a2, 0.4);
}
wait(1.0);
// break the filament of one lamp
cut(fig, L1, 1.0);
par {
// the lamp comes back to the page — it was never removed, only broken
show(fig.L1, 0.4);
// `show`, not `draw`: `hidden` is an OPACITY, and `draw` animates trace — a hidden entity
// traced all the way in is still invisible
show(break1, 0.35);
fade(bulb, 0.6);
fade(l1, 0.6);
}
run(fig, 4.0);
wait(0.4);
par {
show(a3, 0.4);
show(a4, 0.4);
show(a5, 0.4);
}
wait(1.4);
// mend it, and the whole loop lights again
par {
fade(break1, 0.4);
reconnect(fig, L1, 1.0);
}
run(fig, 4.0);
wait(0.9);
circuit-does-current-flow
The teaching beat end to end: a battery, a switch, a lamp and the wires between them
drawn in a teacher’s order, the lamp lighting the moment the last wire closes the loop
(because that prefix was solved), a zoom+cam+framebox close-up, then cut on the
return wire — dots stop, lamp dark, probe to zero — and reconnect.
// Does current flow?
//
// The oldest question in electricity, answered by a circuit that draws itself and then gets
// taken apart. Every number on screen is solved: the schematic is built one component at a
// time and each PREFIX of it is analysed, so nothing moves until the loop closes — and when
// the return wire is cut, the lamp's current goes to zero because the circuit that is left
// was solved from scratch, not because anything was dimmed.
//
// Nothing here is a circuit-specific animation verb. The drawing, the framing and the
// close-up are Manic's core kit; the physics is Modified Nodal Analysis.
title("does current flow?");
canvas("16:9");
template("black");
text(brand, (640, 44), "maniclang.com");
display(brand);
size(brand, 22);
color(brand, dim);
circuit(loop, (600, 400), `
dc-voltage 0 6 0 0 v=9
wire 0 0 4 0
switch 4 0 7 0 closed=1
wire 7 0 11 0
lamp 11 0 11 6 r=180 name=L1
wire 11 6 6 6 name=RET
wire 6 6 0 6
`, 58);
// The animator's dial on the current: a little quicker than the circuit's own pace, because a
// short film wants pace. `speed` scales every branch by the same factor, so the ratio between
// branches — the only thing dot speed is allowed to mean — is untouched. `current` also takes a
// shape (circle, square, diamond), a colour (any palette name or #rrggbb) and a size.
current(loop, 1.6);
probe(loop, L1, (70, -6));
// the highlight box is declared up front and hidden — a ctor belongs above the timeline
framebox(ring, loop.L1, 10);
hidden(ring);
text(q1, (640, 118), "one loop, one lamp");
hidden(q1);
size(q1, 30);
color(q1, cyan);
text(q2, (640, 118), "cut the return: still a lamp, no current");
hidden(q2);
size(q2, 30);
color(q2, magenta);
text(q3, (640, 118), "put it back");
hidden(q3);
size(q3, 30);
color(q3, lime);
// the schematic draws itself in netlist order, and the current starts by itself when the
// last wire closes the loop
show(q1, 0.5);
run(loop, 7.0);
wait(0.4);
// look closely at the one component that is doing the work
par {
zoom(1.7, 1.2);
cam((820, 420), 1.2);
show(ring, 0.8);
}
wait(1.0);
par {
zoom(1.0, 1.0);
cam((640, 360), 1.0);
fade(ring, 0.6);
}
// take the return wire out — and the lamp goes dark because there is nothing to light it
par {
fade(q1, 0.4);
show(q2, 0.4);
}
cut(loop, RET, 0.9);
run(loop, 3.5);
wait(0.6);
// and back
par {
fade(q2, 0.4);
show(q3, 0.4);
}
reconnect(loop, RET, 0.9);
run(loop, 3.5);
wait(0.8);
circuit-kit-promo
Six circuits on a textbook page, each erasing itself, drawing itself back, naming itself
and running its own current — Ohm’s law, a divider, RC, RL, a rectifier and an LED — then
the whole board alive at once. Six current(...) styles: shape, colour and pace, with the
proportionality between branches untouched.
// The circuit kit, in six circuits.
//
// A contact sheet that comes alive: six real circuits on screen from the first frame, then each
// one erases itself, draws itself back, names itself and runs its own current — a different
// shape, colour and pace for every panel. The last beat runs all six at once.
//
// Every dot on screen is a charge integral of a solved branch current, so the six panels are
// running at honestly different speeds because their currents differ, not because six numbers
// were typed. Nothing here is a circuit-specific animation verb: erase, draw, show and par are
// Manic's core kit.
title("six circuits");
canvas("16:9");
template("black");
text(brand, (640, 40), "maniclang.com");
display(brand);
size(brand, 22);
color(brand, dim);
// ── the board: complete from t = 0, so the first frame is already the whole kit ──
//
// No `ground` line anywhere: a ground is a REFERENCE, not a component. MNA needs one node to
// measure from, and with none written the kit elects the source's negative terminal — so a single
// loop solves identically either way, and the symbol would only be clutter. Write one when a
// circuit has several sources or loops and you want to say which node is zero.
circuit(ohm, (250, 240), `
dc-voltage 0 4 0 0 v=9
resistor 0 0 4 0 r=1k
wire 4 0 4 4
wire 4 4 0 4
`, 40, 1, 0);
circuit(divider, (640, 240), `
dc-voltage 0 4 0 0 v=9
resistor 0 0 4 0 r=3k
resistor 4 0 4 4 r=1k
wire 4 4 0 4
`, 40, 1, 0);
circuit(rc, (1030, 240), `
dc-voltage 0 4 0 0 v=5
resistor 0 0 4 0 r=1k
capacitor 4 0 4 4 c=10u
wire 4 4 0 4
`, 40, 1, 0);
circuit(rl, (250, 512), `
dc-voltage 0 4 0 0 v=5
resistor 0 0 4 0 r=100
inductor 4 0 4 4 l=10m
wire 4 4 0 4
`, 40, 1, 0);
circuit(rect, (640, 512), `
ac-voltage 0 4 0 0 v=5 f=60
diode 0 0 3 0
resistor 3 0 3 4 r=1k
wire 3 4 0 4
`, 40, 1, 0);
circuit(led, (1030, 512), `
dc-voltage 0 4 0 0 v=5
resistor 0 0 3 0 r=330
led 3 0 3 4
wire 3 4 0 4
`, 40, 1, 0);
// ── a different current in every panel: shape, colour, pace ──
//
// Inks chosen for the `paper` template: on cream, gold and amber wash out, so these are the
// darker end of the palette — the colours a textbook would actually print in.
current(ohm, 1, circle, crimson, 3);
current(divider, 1.5, circle, indigo, 3);
current(rc, 1.2, square, green, 3);
current(rl, 2, diamond, purple, 4);
current(rect, 1.6, diamond, orange, 4);
current(led, 2.5, circle, magenta, 4);
// ── the names, which arrive as each panel takes its turn ──
text(n1, (250, 352), "Ohm's law");
hidden(n1);
size(n1, 24);
color(n1, crimson);
text(n2, (640, 352), "voltage divider");
hidden(n2);
size(n2, 24);
color(n2, indigo);
text(n3, (1030, 352), "RC charging");
hidden(n3);
size(n3, 24);
color(n3, green);
text(n4, (250, 624), "RL current rise");
hidden(n4);
size(n4, 24);
color(n4, purple);
text(n5, (640, 624), "half-wave rectifier");
hidden(n5);
size(n5, 24);
color(n5, orange);
text(n6, (1030, 624), "LED + series resistor");
hidden(n6);
size(n6, 24);
color(n6, magenta);
// ── six beats: clear to nothing, draw, name, run ──
//
// The erase has to go to a real zero state, and that means addressing the RIGHT tags. `erase` is
// a stroke verb — it traces a shape out — so on the bare circuit id it would take the component
// strokes away and leave the value labels and the charge dots sitting there, and a text entity
// under `trace` reveals PART of its characters ("10mH" erasing down to "1"). So the strokes are
// erased, and everything that is not a stroke is faded.
wait(0.8);
par {
erase(ohm.parts, 0.35);
fade(ohm.labels, 0.3);
fade(ohm.charge, 0.2);
}
par {
draw(ohm.parts, 0.85);
show(ohm.labels, 0.5);
}
show(n1, 0.3);
run(ohm, 2.0);
par {
erase(divider.parts, 0.35);
fade(divider.labels, 0.3);
fade(divider.charge, 0.2);
}
par {
draw(divider.parts, 0.85);
show(divider.labels, 0.5);
}
show(n2, 0.3);
run(divider, 2.0);
par {
erase(rc.parts, 0.35);
fade(rc.labels, 0.3);
fade(rc.charge, 0.2);
}
par {
draw(rc.parts, 0.85);
show(rc.labels, 0.5);
}
show(n3, 0.3);
run(rc, 2.0);
par {
erase(rl.parts, 0.35);
fade(rl.labels, 0.3);
fade(rl.charge, 0.2);
}
par {
draw(rl.parts, 0.85);
show(rl.labels, 0.5);
}
show(n4, 0.3);
run(rl, 2.0);
par {
erase(rect.parts, 0.35);
fade(rect.labels, 0.3);
fade(rect.charge, 0.2);
}
par {
draw(rect.parts, 0.85);
show(rect.labels, 0.5);
}
show(n5, 0.3);
run(rect, 2.0);
par {
erase(led.parts, 0.35);
fade(led.labels, 0.3);
fade(led.charge, 0.2);
}
par {
draw(led.parts, 0.85);
show(led.labels, 0.5);
}
show(n6, 0.3);
run(led, 2.0);
// ── and the whole board alive at once ──
wait(0.3);
par {
run(ohm, 5.0);
run(divider, 5.0);
run(rc, 5.0);
run(rl, 5.0);
run(rect, 5.0);
run(led, 5.0);
}
wait(0.7);
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);
parameterized-field-advection
One parameter smoothly deforms a persistent vector field from steady wind into a vortex.
The generic advect verb then moves real seeded tracers through that same settled field.
// PARAMETERIZED VECTOR FIELD — one value changes the whole field.
// The same settled field then moves a persistent tracer collection. No frame
// callbacks and no manually authored particle paths are involved.
title("From Wind to Vortex");
canvas("16:9");
template("mono");
watermark(mark, (170, 55), "Made With Manic");
text(kicker, (640, 48), "MANIC · PARAMETERIZED FIELD");
text(headline, (640, 94), "One parameter reshapes every arrow");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
rect(chamber, (640, 365), 780, 410);
outlined(chamber); outline(chamber, dim); stroke(chamber, 3);
// The initial named field supplies a clear first state. `bind` then gives it
// formula components where x,y are field coordinates and p is the parameter.
vectorfield(flow, (640, 365), 375, 190, uniform, 17);
parameter(mix, (640, 610), 0, 0, 1, "vortex mix", 2);
bind(mix, flow, formula, "(1-p)*1.15-p*y", "p*x");
circle(seed, (640, 365), 128);
outlined(seed); outline(seed, dim); dashed(seed, 10, 9);
particles(tracers, seed, 32, 5, 31, "random");
color(tracers, fg); glow(tracers, 0.55);
text(caption, (640, 678), "p = 0: steady wind");
size(caption, 20); color(caption, dim);
untraced(flow); hidden(seed); hidden(tracers);
step("reveal-the-field") {
par {
draw(flow, 0.9);
show(seed, 0.4);
show(tracers, 0.55);
}
}
wait(0.5);
step("wind-becomes-vortex") {
par {
to(mix, value, 1, 2.5, smooth);
say(caption, "p = 1: the same field turns into a vortex", 0.4);
}
}
wait(0.55);
step("release-tracers") {
par {
advect(tracers, flow, 4.4, 0.78);
say(caption, "The dots follow the settled field — their paths were never drawn.", 0.4);
}
}
wait(1.2);
gradient-descent-field
Twelve imperfect guesses begin without the answer. A formula-authored negative-gradient field gives each one local corrections until their different journeys discover one shared minimum—then a creator CTA turns optimization into a complete learning story.
// GRADIENT DESCENT AS A LEARNING STORY
// Twelve guesses begin without the answer. Every arrow supplies one local
// correction, and the same authored field carries all of them to one minimum.
title("How a Wrong Guess Learns");
canvas("9:16");
template("blank");
watermark(mark,(w*0.16,h*0.045),"Made With Manic");
text(kicker,(cx,h*0.075),"GRADIENT DESCENT · LEARNING AS MOTION");
text(headline,(cx,h*0.125),"How does a wrong guess find the minimum?");
text(chapter,(cx,h*0.265),"1 · START ANYWHERE");
text(caption,(cx,h*0.78),"Twelve guesses begin with no map of the whole landscape.");
text(result,(cx,h*0.715),"12 STARTING POINTS → 1 SHARED MINIMUM");
text(cta,(cx,h*0.91),"MAKE LEARNING VISIBLE → 8gwifi.org/manic");
size(kicker,18); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,32); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(chapter,19); color(chapter,cyan); bold(chapter); hidden(chapter);
size(caption,21); color(caption,dim); wrap(caption,w*0.84); hidden(caption);
size(result,19); color(result,lime); bold(result); hidden(result);
size(cta,21); color(cta,cyan); bold(cta); hidden(cta);
equation(lossRule,(cx,h*0.205),`L(x,y)=\frac{1}{2}x^2+y^2`);
size(lossRule,34); color(lossRule,fg); hidden(lossRule);
let fieldY = h*0.49;
rect(domain,(cx,fieldY),w*0.82,h*0.40);
outlined(domain); outline(domain,dim); stroke(domain,3);
hidden(domain);
// -grad L = (-x,-2y). The gentler vertical coefficient keeps the motion
// readable while preserving the elliptical convergence of the loss bowl.
vectorfield(descent,(cx,fieldY),w*0.37,h*0.175,"-x","-1.6*y",17);
untraced(descent);
circle(starts,(cx,fieldY),w*0.255);
outlined(starts); outline(starts,dim); dashed(starts,12,10);
hidden(starts);
particles(guesses,starts,12,8,73,"ring");
color(guesses,magenta); glow(guesses,0.78);
hidden(guesses);
circle(minimum,(cx,fieldY),14);
filled(minimum); color(minimum,lime); glow(minimum,1.0);
hidden(minimum);
circle(targetRing,(cx,fieldY),44);
outlined(targetRing); outline(targetRing,lime); stroke(targetRing,3);
hidden(targetRing);
text(minLabel,(cx,fieldY+74),"minimum");
size(minLabel,19); color(minLabel,lime); bold(minLabel); hidden(minLabel);
step("begin with imperfect guesses") {
par {
show(kicker,0.30);
show(headline,0.45);
show(lossRule,0.45);
show(chapter,0.35);
show(caption,0.40);
show(domain,0.45);
show(starts,0.45);
show(guesses,0.60);
}
}
wait(0.65);
step("reveal one local clue everywhere") {
par {
draw(descent,1.10);
fade(starts,0.45);
say(chapter,"2 · READ THE LOCAL SLOPE",0.35);
say(caption,"Every arrow gives one instruction: move opposite the local gradient.",0.42);
}
}
wait(0.55);
step("improve one small step at a time") {
par {
advect(guesses,descent,6.80,0.32);
seq {
say(chapter,"3 · FOLLOW THE CORRECTIONS",0.35);
say(caption,"Far away, the correction is large. Near the bottom, each adjustment becomes smaller.",0.42);
wait(3.05);
say(caption,"Different starting points take different journeys—but obey the same rule.",0.42);
}
}
}
wait(0.55);
step("discover the shared destination") {
par {
show(minimum,0.35);
show(targetRing,0.40);
show(minLabel,0.35);
show(result,0.45);
say(chapter,"4 · ARRIVE WITHOUT TELEPORTING",0.35);
say(caption,"Gradient descent is not a jump to the answer. It is repeated, measured improvement.",0.42);
seq {
pulse(targetRing,0.70);
pulse(guesses,0.70);
}
}
}
wait(0.75);
step("create with Manic") {
par {
pulse(minimum,0.75);
show(cta,0.45);
say(caption,"Describe the rule once. Manic keeps every guess, arrow, correction, and learning step connected.",0.45);
}
}
wait(1.45);
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).
geo-pythagoras-short
A vertical Short proving a² + b² = c² by area — and a demo that a shader art panel and
generic geometry share ONE frame at full value. The top third is a living domain-warped
aurora plasma (pure per-pixel art, black below a crisp seam); the bottom hosts a 3-4-5 right
triangle that is DRAWN OUT edge by edge — untraced(id) starts each polygon at 0% so
draw(id, dur) pens its outline on while the fill washes in — then squares bloom on the
legs (9, 16) and the hypotenuse (25) until 9 + 16 = 25 → a² + b² = c². Split-stage compo,
creator branding, RaTeX equations.
// geo-pythagoras-short — a SPLIT STAGE: the top third of the frame is a living
// generative `shader` artwork (domain-warped aurora plasma — pure art, showing off what
// per-pixel shaders do), and the bottom two-thirds is the most famous theorem in
// geometry drawn out on black: a 3-4-5 right triangle whose squares bloom to 9 + 16 = 25
// → a² + b² = c². Two concepts, one frame — the shader and the maths each at full value.
//
// manic examples/geo-pythagoras-short.manic
title("Why a² + b² = c²");
canvas("9:16");
template("black");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Try_it_free safe=reels");
// ===================== TOP STAGE : the shader as ART =====================
// A bold domain-warped flow field confined to the top band (v < BAND); pure black
// below it, with a crisp accent rule marking the seam. This is the shader showcase —
// motion, depth and colour, all one closed-form expression per pixel.
shader(art) {
let band = 0.34;
let inside = 1.0 - smoothstep(band - 0.02, band, v); // 1 in the top band, 0 below
// domain warp: fold the plane through itself twice for turbulent, liquid flow
let x = (u - 0.5) * asp;
let y = v / band; // 0 at top → 1 at the seam
let w1 = sin(x * 3.0 + t * 0.5) + cos(y * 4.0 - t * 0.3);
let w2 = sin((x + w1) * 2.6 - t * 0.4) * cos((y + w1) * 3.2 + t * 0.25);
let flow = 0.5 + 0.5 * sin(x * 4.0 + w2 * 2.2 + t * 0.6);
let bands = 0.5 + 0.5 * sin(y * 9.0 - w2 * 3.0 + t * 0.5); // aurora curtains
let hue = mod(244 + flow * 118 + w2 * 44, 360); // magenta → cyan aurora
let sat = 0.82;
let base = (0.14 + 0.62 * flow * bands) * inside;
// crisp bright accent rule along the seam (the two stages meet here)
let rule = smoothstep(0.006, 0.0, abs(v - band));
let val = base + rule * 0.85;
}
// ===================== BOTTOM STAGE : the proof on black =====================
// geometry of the 3-4-5 right triangle, sized + placed to live entirely below the seam
let ux = w * 0.070; // one unit
let ox = w * 0.36; // right-angle vertex x
let oy = h * 0.75; // right-angle vertex y (well below the art band)
// vertices: C (right angle), A (end of the 4-leg), B (end of the 3-leg)
point(pC, (ox, oy));
point(pA, (ox + 4*ux, oy));
point(pB, (ox, oy - 3*ux));
hidden(pC); hidden(pA); hidden(pB);
polygon(tri, (ox, oy), (ox + 4*ux, oy), (ox, oy - 3*ux), cyan);
rightangle(ra, pA, pC, pB);
// `untraced` starts a shape drawn-to-0% (opacity stays 1) so `draw` can pen it on —
// the outline traces edge by edge while the fill washes in.
untraced(tri); hidden(ra);
// squares on the two legs (areas 16 and 9) and on the hypotenuse (area 25)
polygon(sqA, (ox, oy), (ox + 4*ux, oy), (ox + 4*ux, oy + 4*ux), (ox, oy + 4*ux), gold);
polygon(sqB, (ox, oy), (ox, oy - 3*ux), (ox - 3*ux, oy - 3*ux), (ox - 3*ux, oy), lime);
polygon(sqC, (ox + 4*ux, oy), (ox + 7*ux, oy - 4*ux), (ox + 3*ux, oy - 7*ux), (ox, oy - 3*ux), magenta);
untraced(sqA); untraced(sqB); untraced(sqC);
// side length labels — on the triangle's own edges (dark ink on the cyan face)
caption(lb, "3", (ox + 26, oy - 1.5*ux), 32); color(lb, #06202a);
caption(la, "4", (ox + 2*ux, oy - 28), 32); color(la, #06202a);
caption(lc, "5", (ox + 2.6*ux, oy - 1.9*ux), 32); color(lc, #06202a);
hidden(la); hidden(lb); hidden(lc);
// area labels, sitting in each square (dark ink on the bright leg-squares, white on the hyp)
caption(cA, "16", (ox + 2*ux, oy + 2*ux), 44); color(cA, #101018);
caption(cB, "9", (ox - 1.5*ux, oy - 1.5*ux), 44); color(cB, #101018);
caption(cC, "25", (ox + 3.5*ux, oy - 3.5*ux), 44); color(cC, white);
hidden(cA); hidden(cB); hidden(cC);
// the payoff equations — tucked under the diagram, along the bottom edge
equation(eqn, (w*0.5, h*0.955), `9 + 16 = 25`, 46); hidden(eqn);
equation(eq2, (w*0.5, h*0.955), `a^2 + b^2 = c^2`, 46); hidden(eq2);
// hook + point text — in the seam gap between the art band and the diagram
caption(hook, "Why is this ALWAYS a right triangle?", (w*0.5, h*0.42), 32);
caption(point, "Every right triangle. Always.", (w*0.5, h*0.42), 34);
hidden(hook); hidden(point);
// ================= script =================
show(hook);
wait(2.4);
fade(hook, 0.4);
// BUILD — the triangle is DRAWN OUT edge by edge, then the right angle + side labels
draw(tri, 1.4);
wait(0.4);
show(ra);
wait(0.4);
show(la); show(lb); show(lc);
wait(1.2);
// RULE — squares DRAW out on the legs (pen tracing each side), then the areas appear
draw(sqA, 1.1); draw(sqB, 1.1);
wait(0.5);
show(cA); show(cB);
wait(1.4);
// then the hypotenuse square draws out — the payoff shape
draw(sqC, 1.3);
wait(0.5);
show(cC);
wait(1.6);
// TWIST — the two small squares add up to the big one
pulse(sqA, 1.4); pulse(sqB, 1.4);
show(eqn);
wait(1.6);
pulse(sqC, 1.6);
fade(eqn, 0.4);
show(eq2);
wait(2.0);
// POINT + hold
show(point);
wait(2.6);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
circle-in-three
A Problem Book plate (paper + masthead): split one round cake among three friends with exactly
TWO straight cuts. Wrong guesses first, then the lemma (crossing cuts always give four pieces), then
the transcendental equation cos⁻¹(u) − u√(1−u²) = π/3 solved on a live graph, then the fair cut —
with the three piece percentages recomputing throughout. The solved height u = a/R = 0.2649.
// circle-in-three — a story-driven textbook plate: divide one round cake among
// three friends using exactly two straight cuts. Wrong ideas first, then the
// lemma (crossing cuts always give four pieces), then the transcendental
// equation, solved on a graph, then the fair cut - live percentages throughout.
// The solved height: cos^-1(u) - u sqrt(1-u^2) = pi/3 => u = a/R = 0.2649.
title("Split a Circle in Three - with Two Lines");
canvas("16:9");
template("paper");
masthead("The Problem Book", "Plate VII");
// ---------- layout ----------
let R = 205;
let ox = 400;
let oy = 400;
let dyE = R/3; // "evenly spaced" cut height (the wrong guess)
let dyS = 0.2649*R; // the solved cut height
let sf = 1.0228; // chord length ratio: sqrt(1-0.2649^2)/sqrt(8/9)
// ---------- HUD ----------
text(head, (cx, 100), "Split a Circle in Three - with Two Lines"); display(head); cursor(head); sticky(head);
text(cap, (cx, h - 40), ""); size(cap, 26); sticky(cap);
text(fig, (ox, 648), ""); size(fig, 24); color(fig, dim); sticky(fig);
// ---------- the cake ----------
circle(cake, (ox, oy), R); outlined(cake); stroke(cake, 3); untraced(cake);
// attempt 1: two diameters
line(d1, (ox, oy - R), (ox, oy + R)); untraced(d1); stroke(d1, 3);
line(d2, (ox - R, oy), (ox + R, oy)); untraced(d2); stroke(d2, 3);
text(q1, (ox + 70, oy - 70), "1"); size(q1, 30); hidden(q1); tag(q1, qlab);
text(q2, (ox - 70, oy - 70), "2"); size(q2, 30); hidden(q2); tag(q2, qlab);
text(q3, (ox - 70, oy + 70), "3"); size(q3, 30); hidden(q3); tag(q3, qlab);
text(q4, (ox + 70, oy + 70), "4"); size(q4, 30); hidden(q4); tag(q4, qlab);
// attempt 2: parallel cuts at thirds (they will SLIDE to the true height later)
line(cA1, (ox - R*0.94281, oy - dyE), (ox + R*0.94281, oy - dyE)); untraced(cA1); stroke(cA1, 4); color(cA1, red);
line(cB1, (ox - R*0.94281, oy + dyE), (ox + R*0.94281, oy + dyE)); untraced(cB1); stroke(cB1, 4); color(cB1, red);
// boolean sources (hidden): the disc and the horizontal bands
circle(disc, (ox, oy), R); hidden(disc);
rect(rT2, (ox, oy - dyE - 75), 430, 150); hidden(rT2);
rect(rM2, (ox, oy), 430, 2*dyE); hidden(rM2);
rect(rB2, (ox, oy + dyE + 75), 430, 150); hidden(rB2);
rect(rT3, (ox, oy - dyS - 75), 430, 150); hidden(rT3);
rect(rM3, (ox, oy), 430, 2*dyS); hidden(rM3);
rect(rB3, (ox, oy + dyS + 75), 430, 150); hidden(rB3);
// the three shares, evenly-spaced version (29.2 / 41.7 / 29.2)
intersect(capT2, disc, rT2); color(capT2, cyan); opacity(capT2, 0.45); hidden(capT2); tag(capT2, att2);
intersect(mid2, disc, rM2); color(mid2, gold); opacity(mid2, 0.45); hidden(mid2); tag(mid2, att2);
intersect(capB2, disc, rB2); color(capB2, magenta); opacity(capB2, 0.45); hidden(capB2); tag(capB2, att2);
// the three shares, solved version (33.3 each)
intersect(capT3, disc, rT3); color(capT3, cyan); opacity(capT3, 0.45); hidden(capT3); tag(capT3, att3);
intersect(mid3, disc, rM3); color(mid3, gold); opacity(mid3, 0.45); hidden(mid3); tag(mid3, att3);
intersect(capB3, disc, rB3); color(capB3, magenta); opacity(capB3, 0.45); hidden(capB3); tag(capB3, att3);
// live percentages, one per share
counter(pcT, (ox, oy - 128), 0, 1, "", "%"); hidden(pcT);
counter(pcM, (ox, oy), 0, 1, "", "%"); hidden(pcM);
counter(pcB, (ox, oy + 128), 0, 1, "", "%"); hidden(pcB);
// ---------- the mathematics panel (right side) ----------
equation(eqA, (960, 205), `A_{\text{cap}}(a) \;=\; R^2\cos^{-1}\!\frac{a}{R} \;-\; a\sqrt{R^2-a^2}`, 32); hidden(eqA); sticky(eqA);
equation(eqG, (960, 430), `f(u)=\cos^{-1}u \;-\; u\sqrt{1-u^2} \;-\; \tfrac{\pi}{3}`, 26); hidden(eqG); sticky(eqG);
arrow(gxa, (850, 560), (1195, 560)); untraced(gxa); stroke(gxa, 2);
arrow(gya, (880, 655), (880, 495)); untraced(gya); stroke(gya, 2);
plot(pf, (880, 560), 300, 90, "acos(x)-x*sqrt(1-x^2)-1.047198", (0.01, 0.99));
untraced(pf); stroke(pf, 3); color(pf, blue);
roots(rt, pf, magenta); hidden(rt);
text(rlab, (1040, 596), "u ≈ 0.2649"); size(rlab, 24); color(rlab, magenta); hidden(rlab);
// ================= timeline =================
// ---- the problem
type(head, 1.1);
say(cap, "Three friends. One round cake. Two straight cuts. Make it fair.", 0.6);
draw(cake, 1.2);
say(fig, "Fig. 1 - the cake", 0.3);
wait(0.5);
// ---- attempt 1: through the middle, twice
cue(whoosh);
say(cap, "First idea: cut through the middle - twice.", 0.5);
par { draw(d1, 0.7); draw(d2, 0.7); }
stagger(0.12) { show(q1, 0.3); show(q2, 0.3); show(q3, 0.3); show(q4, 0.3); }
say(cap, "Four pieces. Perfectly equal - and perfectly useless. We are THREE.", 0.5);
wait(0.6);
// ---- the lemma: crossing cuts always make four
say(cap, "Tilt a cut, slide it anywhere: two CROSSING cuts always make four pieces.", 0.5);
rotate(d2, 38, 1.0, smooth);
shift(d2, (55, 35), 1.0, smooth);
pulse(qlab, 0.8);
say(cap, "Lemma: the two cuts must never cross inside the cake. They must be PARALLEL.", 0.5);
cue(tick);
par { fade(d1, 0.5); fade(d2, 0.5); fade(qlab, 0.4); }
// ---- attempt 2: parallel, evenly spaced
say(fig, "Fig. 2 - parallel cuts at thirds", 0.3);
say(cap, "Second idea: space the parallel cuts evenly - at thirds of the diameter.", 0.5);
par { draw(cA1, 0.7); draw(cB1, 0.7); }
stagger(0.15) { show(capT2, 0.5); show(mid2, 0.5); show(capB2, 0.5); }
par { show(pcT, 0.3); show(pcM, 0.3); show(pcB, 0.3); }
par { to(pcT, value, 29.2, 1.3, smooth); to(pcM, value, 41.7, 1.3, smooth); to(pcB, value, 29.2, 1.3, smooth); }
cue(pop);
flash(mid2, gold);
say(cap, "The middle friend grins: 41.7 percent. Evenly SPACED is not evenly SHARED.", 0.5);
wait(0.8);
// ---- the mathematics: ask the cake
say(cap, "So ask the cake: how much lies beyond a cut at height a?", 0.5);
show(eqA, 0.6);
wait(0.8);
cue(tick);
rewrite(eqA, `R^2\cos^{-1}\!\frac{a}{R} - a\sqrt{R^2-a^2} \;=\; \frac{\pi R^2}{3}`, 0.9);
say(cap, "Each outer piece must hold a third of the cake. Divide out the radius...", 0.5);
cue(tick);
rewrite(eqA, `\cos^{-1}u \;-\; u\sqrt{1-u^2} \;=\; \frac{\pi}{3}, \qquad u=\frac{a}{R}`, 0.9);
say(cap, "No algebra unties this knot - it is transcendental. Let the graph solve it.", 0.5);
say(fig, "Fig. 3 - solving f(u) = 0", 0.3);
par { draw(gxa, 0.5); draw(gya, 0.5); }
par { draw(pf, 1.3); show(eqG, 0.7); }
show(rt, 0.4); show(rlab, 0.4);
par { cam((959, 545), 1.0, smooth); zoom(1.6, 1.0, smooth); }
flash(rt, magenta);
say(cap, "One root. The cuts belong at u = 0.2649 - about a quarter of the radius.", 0.5);
wait(0.7);
par { cam((cx, cy), 1.0, smooth); zoom(1, 1.0, smooth); }
cue(tick);
rewrite(eqA, `u \;=\; \frac{a}{R} \;\approx\; 0.2649`, 0.9);
// ---- the fair cut: slide the knives home
say(cap, "Nudge each cut toward the centre: from 0.333 R in to 0.2649 R.", 0.5);
cue(whoosh);
par {
shift(cA1, (0, dyE - dyS), 1.4, smooth); scale(cA1, sf, 1.4, smooth);
shift(cB1, (0, dyS - dyE), 1.4, smooth); scale(cB1, sf, 1.4, smooth);
fade(att2, 1.2); show(capT3, 1.2); show(mid3, 1.2); show(capB3, 1.2);
}
par { to(pcT, value, 33.3, 1.3, smooth); to(pcM, value, 33.3, 1.3, smooth); to(pcB, value, 33.3, 1.3, smooth); }
cue(chime);
par { flash(capT3, cyan); flash(mid3, gold); flash(capB3, magenta); }
say(cap, "33.3 - 33.3 - 33.3. Fair, to the crumb.", 0.5);
say(fig, "Fig. 4 - the fair cut: a = 0.2649 R", 0.3);
wait(0.8);
// ---- closing plate
cue(tick);
rewrite(eqA, `\boxed{\;a \;=\; 0.2649\,R\;}`, 1.0);
par {
breathe(eqA, 3, 0.05, 0, 5);
seq { pulse(capT3, 0.7); pulse(mid3, 0.7); pulse(capB3, 0.7); }
}
say(cap, "Two cuts. Three equal shares. One transcendental number.", 0.6);
wait(2.5);
circle-thirds-no-triangle
Problem Book Plate VIII: cut a circle into three EQUAL areas — but the classic three-sector answer (and the equilateral triangle hiding inside it) is banned. Two triangle-free solutions: the bullseye (concentric circles at r₁ = R/√3, r₂ = R√(2/3)) and the waves (S-curves of semicircular arcs on a thirds-divided diameter), each piece proven to equal πR²/3 exactly. A 1,000-entity constructed proof.
// circle-thirds-no-triangle — Plate VIII of the Problem Book: cut a circle
// into three EQUAL areas, but the classic three-sector answer (and the
// equilateral triangle hiding inside it) is banned. Two triangle-free answers:
// 1. the bullseye: concentric circles at r1 = R/sqrt(3), r2 = R sqrt(2/3)
// 2. the waves: S-curves of semicircular arcs on a thirds-divided diameter;
// piece_k = (pi/2)(r_k^2 - r_{k-1}^2) + (pi/2)(s_{k-1}^2 - s_k^2)
// = (pi R^2/18)[(2k-1) + (7-2k)] = pi R^2 / 3 for EVERY k.
title("Three Equal Parts - No Triangles Allowed");
canvas("16:9");
template("paper");
masthead("The Problem Book", "Plate VIII");
// ---------- layout ----------
let R = 205;
let ox = 400;
let oy = 400;
let r1 = R*0.57735; // R / sqrt 3
let r2 = R*0.8165; // R sqrt(2/3)
// ---------- HUD ----------
text(head, (cx, 100), "Three Equal Parts - No Triangles Allowed"); display(head); cursor(head); sticky(head);
text(cap, (cx, h - 40), ""); size(cap, 26); sticky(cap);
text(fig, (ox, 648), ""); size(fig, 24); color(fig, dim); sticky(fig);
// ---------- the cake ----------
circle(cake, (ox, oy), R); outlined(cake); stroke(cake, 3); untraced(cake);
circle(disc, (ox, oy), R); hidden(disc); // hidden filled source for booleans
// ---------- act 1: the banned answer ----------
line(m1, (ox, oy), (ox, oy - R)); untraced(m1); stroke(m1, 3); tag(m1, merc);
line(m2, (ox, oy), (ox - R*0.866, oy + R*0.5)); untraced(m2); stroke(m2, 3); tag(m2, merc);
line(m3, (ox, oy), (ox + R*0.866, oy + R*0.5)); untraced(m3); stroke(m3, 3); tag(m3, merc);
line(e1, (ox, oy - R), (ox - R*0.866, oy + R*0.5)); dashed(e1); stroke(e1, 3); color(e1, red); untraced(e1); tag(e1, tri);
line(e2, (ox - R*0.866, oy + R*0.5), (ox + R*0.866, oy + R*0.5)); dashed(e2); stroke(e2, 3); color(e2, red); untraced(e2); tag(e2, tri);
line(e3, (ox + R*0.866, oy + R*0.5), (ox, oy - R)); dashed(e3); stroke(e3, 3); color(e3, red); untraced(e3); tag(e3, tri);
// ---------- act 2: the bullseye ----------
circle(c1, (ox, oy), r1); outlined(c1); stroke(c1, 3); untraced(c1); tag(c1, bull);
circle(c2, (ox, oy), r2); outlined(c2); stroke(c2, 3); untraced(c2); tag(c2, bull);
circle(c1d, (ox, oy), r1); hidden(c1d);
circle(c2d, (ox, oy), r2); hidden(c2d);
circle(fin, (ox, oy), r1); color(fin, cyan); opacity(fin, 0.45); hidden(fin); tag(fin, bull);
difference(ringM, c2d, c1d); color(ringM, gold); opacity(ringM, 0.45); hidden(ringM); tag(ringM, bull);
difference(ringO, disc, c2d); color(ringO, magenta); opacity(ringO, 0.45); hidden(ringO); tag(ringO, bull);
counter(pb1, (ox, oy), 0, 1, "", "%"); hidden(pb1); tag(pb1, bull);
counter(pb2, (ox, oy - 143), 0, 1, "", "%"); hidden(pb2); tag(pb2, bull);
counter(pb3, (ox, oy + 186), 0, 1, "", "%"); hidden(pb3); tag(pb3, bull);
equation(eqB, (960, 210), `\pi r_1^2 \;=\; \pi\big(r_2^2-r_1^2\big) \;=\; \pi\big(R^2-r_2^2\big) \;=\; \frac{\pi R^2}{3}`, 26); hidden(eqB); sticky(eqB);
// ---------- act 3: the waves ----------
// construction guides: the diameter, divided into thirds
line(diam, (ox - R, oy), (ox + R, oy)); dashed(diam); stroke(diam, 2); color(diam, dim); untraced(diam); tag(diam, guide);
dot(dv1, (ox - R/3, oy), 6); color(dv1, gold); hidden(dv1); tag(dv1, guide);
dot(dv2, (ox + R/3, oy), 6); color(dv2, gold); hidden(dv2); tag(dv2, guide);
// cut 1: semicircle r = R/3 above on the first third + r = 2R/3 below on the rest
plot(pU1, (ox, oy), R, R, "sqrt(abs(0.111111-(x+0.666667)^2))", (-1, -0.3333));
untraced(pU1); stroke(pU1, 4); color(pU1, red); tag(pU1, waves);
plot(pL1, (ox, oy), R, R, "0-sqrt(abs(0.444444-(x-0.333333)^2))", (-0.3333, 1));
untraced(pL1); stroke(pL1, 4); color(pL1, red); tag(pL1, waves);
// cut 2: semicircle r = 2R/3 above on the first two thirds + r = R/3 below
plot(pU2, (ox, oy), R, R, "sqrt(abs(0.444444-(x+0.333333)^2))", (-1, 0.3333));
untraced(pU2); stroke(pU2, 4); color(pU2, red); tag(pU2, waves);
plot(pL2, (ox, oy), R, R, "0-sqrt(abs(0.111111-(x-0.666667)^2))", (0.3333, 1));
untraced(pL2); stroke(pL2, 4); color(pL2, red); tag(pL2, waves);
// boolean sources: half-disc sectors + the four construction circles.
// key fact: consecutive half-discs are INTERNALLY TANGENT, so every wave piece is
// either a plain sector or ONE difference(sector, circle) - no nesting needed.
sector(sLoB, (ox, oy), R, 180, 360); hidden(sLoB);
sector(sHiB, (ox, oy), R, 0, 180); hidden(sHiB);
sector(sU2s, (ox - R/3, oy), 2*R/3, 0, 180); hidden(sU2s);
sector(sL1s, (ox + R/3, oy), 2*R/3, 180, 360); hidden(sL1s);
circle(dU1c, (ox - 2*R/3, oy), R/3); hidden(dU1c);
circle(dU2c, (ox - R/3, oy), 2*R/3); hidden(dU2c);
circle(dL1c, (ox + R/3, oy), 2*R/3); hidden(dL1c);
circle(dL2c, (ox + 2*R/3, oy), R/3); hidden(dL2c);
// region 1 (cyan): lower-left crescent + its little bump above the diameter
difference(w1a, sLoB, dL1c); color(w1a, cyan); opacity(w1a, 0.45); filled(w1a); tag(w1a, wave1); tag(w1a, waves);
sector(sU1f, (ox - 2*R/3, oy), R/3, 0, 180); color(sU1f, cyan); opacity(sU1f, 0.45); filled(sU1f); tag(sU1f, wave1); tag(sU1f, waves);
// region 2 (gold): the S in the middle - one piece above the axis, one below
difference(w2a, sU2s, dU1c); color(w2a, gold); opacity(w2a, 0.45); filled(w2a); tag(w2a, wave2); tag(w2a, waves);
difference(w2b, sL1s, dL2c); color(w2b, gold); opacity(w2b, 0.45); filled(w2b); tag(w2b, wave2); tag(w2b, waves);
// region 3 (magenta): upper-right crescent + its little bump below
difference(w3a, sHiB, dU2c); color(w3a, magenta); opacity(w3a, 0.45); filled(w3a); tag(w3a, wave3); tag(w3a, waves);
sector(sL2f, (ox + 2*R/3, oy), R/3, 180, 360); color(sL2f, magenta); opacity(sL2f, 0.45); filled(sL2f); tag(sL2f, wave3); tag(sL2f, waves);
hidden(wave1); hidden(wave2); hidden(wave3);
counter(pc1, (300, 492), 0, 1, "", "%"); hidden(pc1);
counter(pc2, (370, 360), 0, 1, "", "%"); hidden(pc2);
counter(pc3, (508, 308), 0, 1, "", "%"); hidden(pc3);
equation(eqW, (960, 210), `\text{piece}_k=\frac{\pi}{2}\big(r_k^2-r_{k-1}^2\big)+\frac{\pi}{2}\big(s_{k-1}^2-s_k^2\big)`, 26); hidden(eqW); sticky(eqW); z(eqW, 10);
// ================= timeline =================
// ---- the challenge
type(head, 1.1);
say(cap, "Cut a circle into three equal parts. Everyone knows the answer...", 0.6);
draw(cake, 1.2);
say(fig, "Fig. 1 - the answer everyone knows", 0.3);
par { draw(m1, 0.5); draw(m2, 0.5); draw(m3, 0.5); }
say(cap, "Three cuts from the centre, 120 degrees apart. Equal - by symmetry.", 0.5);
wait(0.5);
say(cap, "But look what hides inside it...", 0.5);
par { draw(e1, 0.5); draw(e2, 0.5); draw(e3, 0.5); }
flash(tri, red);
cue(pop);
say(cap, "An equilateral triangle. And today, triangles are BANNED.", 0.5);
wait(0.5);
par { disintegrate(e1, 0.7); disintegrate(e2, 0.7); disintegrate(e3, 0.7); disintegrate(m1, 0.7); disintegrate(m2, 0.7); disintegrate(m3, 0.7); }
// ---- answer one: the bullseye
cue(whoosh);
say(fig, "Fig. 2 - the bullseye", 0.3);
say(cap, "Answer one: forget cuts across - aim for the BULLSEYE.", 0.5);
par { draw(c1, 0.8); draw(c2, 0.8); }
stagger(0.15) { show(fin, 0.5); show(ringM, 0.5); show(ringO, 0.5); }
par { show(pb1, 0.3); show(pb2, 0.3); show(pb3, 0.3); }
par { to(pb1, value, 33.3, 1.2, smooth); to(pb2, value, 33.3, 1.2, smooth); to(pb3, value, 33.3, 1.2, smooth); }
show(eqB, 0.6);
cue(tick);
rewrite(eqB, `r_1=\frac{R}{\sqrt{3}}\approx 0.577\,R \qquad r_2=R\sqrt{\tfrac{2}{3}}\approx 0.816\,R`, 0.9);
say(cap, "A disc and two rings, each exactly a third. No corners anywhere.", 0.5);
wait(1.2);
par { fade(bull, 0.6); disintegrate(eqB, 0.8); }
// ---- answer two: the waves
cue(whoosh);
say(fig, "Fig. 3 - the waves", 0.3);
say(cap, "Answer two: let the cuts WAVE. Divide the diameter into thirds...", 0.5);
draw(diam, 0.7);
par { show(dv1, 0.4); show(dv2, 0.4); }
say(cap, "...then each cut is two half-circles: one arching over, one diving under.", 0.5);
par { cam((ox, oy), 1.0, smooth); zoom(1.25, 1.0, smooth); }
seq { draw(pU1, 0.9); draw(pL1, 0.9); }
cue(tick);
seq { draw(pU2, 0.9); draw(pL2, 0.9); }
stagger(0.15) { show(wave1, 0.6); show(wave2, 0.6); show(wave3, 0.6); }
par { show(pc1, 0.3); show(pc2, 0.3); show(pc3, 0.3); }
par { to(pc1, value, 33.3, 1.2, smooth); to(pc2, value, 33.3, 1.2, smooth); to(pc3, value, 33.3, 1.2, smooth); }
par { cam((cx, cy), 1.0, smooth); zoom(1, 1.0, smooth); }
// the telescoping proof
show(eqW, 0.6);
say(cap, "Why equal? Each piece is half-circles added and subtracted...", 0.5);
cue(tick);
rewrite(eqW, `\text{piece}_k=\frac{\pi R^2}{18}\Big[(2k-1)+(7-2k)\Big]`, 0.9);
say(cap, "...and the k cancels itself. Every wave weighs the same.", 0.5);
cue(tick);
rewrite(eqW, `\boxed{\;\text{piece}_k=\frac{\pi R^2}{3}\quad\text{for every }k\;}`, 1.0);
cue(chime);
par { flash(wave1, cyan); flash(wave2, gold); flash(wave3, magenta); }
wait(0.8);
// ---- finale: still fair, whichever way it turns
say(fig, "Fig. 4 - still fair, spinning", 0.3);
par { fade(pc1, 0.4); fade(pc2, 0.4); fade(pc3, 0.4); fade(guide, 0.4); }
say(cap, "And a third stays a third, whichever way you turn the plate.", 0.5);
par {
turn(waves, (ox, oy), 120, 5.0, smooth);
breathe(eqW, 3, 0.05, 0, 5);
}
say(cap, "Three equal parts - and not a triangle in sight.", 0.6);
wait(2.0);
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).
gradient
Gradient paint, the acceptance demo: one word gradient(id, c1, c2, …, [mode]) covers
a radial well, a three-stop height-colored plot, a spline colored by "curvature", an
RK4 free kick colored by "speed", and an arc-length arrow whose head takes the tip
color. The color is computed, not painted — and every stop stays template-aware.
// Gradient paint — the color is computed, not painted.
// manic examples/gradient.manic
// manic examples/gradient.manic --still 11 --scale 1.5
//
// One word, five truths (stops ≥ 2, evenly spaced; mode optional):
// gradient(path, c1, c2) arc-length along a stroke
// gradient(plot, c1, c2, c3, 270) linear over the bounds — height, any number of stops
// gradient(shape, c1, c2, radial) centre → edge
// gradient(traj, c1, c2, "speed") a trajectory colored by its true local speed
// gradient(path, c1, c2, "curvature") any path colored by how hard it bends
title("Gradient Paint");
canvas(1280, 720);
template("blank");
// --- cast ---
// a radial "energy well" behind everything
circle(well, (640, 400), 260);
filled(well);
gradient(well, panel, void, radial);
opacity(well, 0.85);
// a plot colored by its own height, in three stops: blue low, cyan mid, gold high.
// The vertical linear gradient over the curve's bounds IS its y-value.
axes(ax, (640, 400), 520, 220);
plot(wave, (640, 400), 78, 150, sin, 6.6);
stroke(wave, 4);
gradient(wave, blue, cyan, gold, 270);
untraced(wave);
// a curve colored by its own curvature — the tight apex carries the color
spline(swoop, (160, 640), (400, 260), (640, 160), (880, 260), (1120, 640));
stroke(swoop, 5);
gradient(swoop, dim, magenta, "curvature");
untraced(swoop);
// an RK4 free kick (top-down pitch) colored by its true speed —
// gold off the boot, blue as drag bites (slowest stop first, fastest last)
freekick(kick, (185, 690), 30, 36, 3.5, 5.5, 120);
stroke(kick, 5);
gradient(kick, blue, gold, "speed");
untraced(kick);
hidden(kick.ball);
// an arrow whose head takes the gradient's tip color
arrow(dart, (420, 600), (1040, 600));
stroke(dart, 4);
gradient(dart, dim, lime);
untraced(dart);
// headline + caption
text(head, (640, 100), "the color is computed, not painted");
display(head); color(head, cyan); size(head, 30); hidden(head);
text(cap, (640, 680), ""); color(cap, dim); size(cap, 22);
// --- script ---
show(head, 0.5);
say(cap, "a height-colored plot: blue low, cyan mid, gold high");
draw(wave, 1.8);
wait(0.6);
say(cap, "a curve colored by its own curvature — the bend glows");
draw(swoop, 1.6);
wait(0.6);
say(cap, "a free kick colored by its true speed — gold fast, blue slow");
draw(kick, 1.6);
wait(0.6);
say(cap, "an arc-length stroke; the arrowhead takes its tip color");
draw(dart, 1.2);
wait(1.2);
svg-import
SVG vector import, the acceptance demo: svg() brings a heart (cubic béziers) and its
highlight (a <circle> normalised to a path) in as NATIVE Polyline/Polygon entities —
the #ff2d95 fill kept LITERAL, not re-themed by the template — then show/pulse/recolor
drive the whole drawing through its {id} tag. Geometry, not a texture; the 2D twin of
model3’s OBJ import.
// SVG vector import — bring artwork in as NATIVE path entities (not a texture).
// Each subpath of heart.svg becomes a traceable Polyline / filled Polygon, fitted
// to 320 px wide and centred; the pieces are tagged `heart`, so ONE verb animates
// the whole drawing. Imported colours reproduce faithfully (the #ff2d95 fill is
// kept literal, not re-themed). This is the 2D twin of `model3`'s OBJ import.
title("SVG Vector Import");
canvas("16:9");
template("neon");
svg(heart, (cx, cy), "asset:svg/heart.svg", 320);
hidden(heart);
show(heart, 0.8); // reveal — real geometry, faithful colours
wait(0.4);
pulse(heart, 0.9); // animate the whole drawing with one verb
wait(0.6);
recolor(heart, cyan); // …and recolour it — imported vectors are first-class
wait(1.2);
svg-howto
“Made of Emoji” — a vector spectacle showing the full power of svg(). Thirty rainbow heart
emoji explode from one point into a ring, then snap into a beating HEART placed by the
parametric heart curve — dozens of imported vectors choreographed by pure math. A rapid
SAMPLER then riffles the bundled catalogue (icons · flags · emoji), flags and heroes rain in
with burst confetti, and a rocket launches to the title. Every actor is one svg() line;
the reveal is scale + computed motion + faithful colour, with optional ElevenLabs VO.
// ============================================================================
// svg-howto.manic — "MADE OF EMOJI"
//
// Not a lecture — a spectacle. From one point, a storm of imported emoji
// explodes, swirls into a galaxy, then snaps into a beating heart — dozens of
// vectors choreographed by pure math, exploded again into a flag-lit finale.
// Every actor is an `svg()` import; nothing is a texture; it's all computed.
//
// manic examples/svg-howto.manic
// manic examples/svg-howto.manic --record out --preset reel
// ============================================================================
title("Made of Emoji — a vector spectacle");
canvas("16:9");
template("blank");
voice("elevenlabs"); // promo narration (ELEVENLABS_API_KEY)
// voice("elevenlabs", "alice");
// voice("elevenlabs", "jessica");
// voice("cartesia"); // voice("gtts"); // free fallback
// speak("…") = voice only; say(sub, "…") = on-screen text; both can run together.
// deep-space backdrop
rect(space, (cx, cy), w*2, h*2); filled(space); gradient(space, void, indigo, radial);
text(sub, (cx, h*0.9), ""); size(sub, 30); color(sub, fg); display(sub); glow(sub, 0.4);
// ---- the swarm: 30 heart emoji, rainbow-hued, all born at the centre ----
for i in 0..30 {
svg(sw{i}, (cx, cy), "asset:svg/emoji/heart.svg", 46);
hue(sw{i}, i*12); // rainbow, one hue apiece
tag(sw{i}, swarm); z(sw{i}, 5); hidden(sw{i});
}
// ---- burst engines (gold ignition, cyan confetti) ----
rect(bseed, (cx, cy), 24, 24); hidden(bseed);
particles(spark, bseed, 30, 6, 22, "random"); color(spark, gold); glow(spark, 0.9); hidden(spark);
rect(cseed, (cx, cy), 24, 24); hidden(cseed);
particles(conf, cseed, 30, 6, 22, "random"); color(conf, cyan); glow(conf, 0.9); hidden(conf);
// ---- the finale cast (flags + hero emoji rain in) ----
svg(rk, (cx, h + 120), "asset:svg/emoji/rocket.svg", 120); z(rk, 9); hidden(rk);
svg(hero0, (cx - 500, -80), "asset:svg/emoji/fire.svg", 80); hidden(hero0);
svg(hero1, (cx - 250, -80), "asset:svg/emoji/star.svg", 80); hidden(hero1);
svg(hero2, (cx + 250, -80), "asset:svg/emoji/party.svg", 80); hidden(hero2);
svg(hero3, (cx + 500, -80), "asset:svg/emoji/trophy.svg", 80); hidden(hero3);
svg(flag0, (cx - 420, -80), "asset:svg/flags/in.svg", 92); hidden(flag0);
svg(flag1, (cx - 140, -80), "asset:svg/flags/br.svg", 92); hidden(flag1);
svg(flag2, (cx + 140, -80), "asset:svg/flags/jp.svg", 92); hidden(flag2);
svg(flag3, (cx + 420, -80), "asset:svg/flags/de.svg", 92); hidden(flag3);
// ---- the sampler: a stack of diverse shapes to riffle through ----
svg(smp0, (cx, cy - 40), "asset:svg/emoji/rocket.svg", 220); hidden(smp0);
svg(smp1, (cx, cy - 40), "asset:svg/emoji/party.svg", 220); hidden(smp1);
svg(smp2, (cx, cy - 40), "asset:svg/emoji/brain.svg", 220); hidden(smp2);
svg(smp3, (cx, cy - 40), "asset:svg/flags/jp.svg", 240); hidden(smp3);
svg(smp4, (cx, cy - 40), "asset:svg/emoji/trophy.svg", 220); hidden(smp4);
svg(smp5, (cx, cy - 40), "asset:svg/lucide/cpu.svg", 200); hue(smp5, 190); hidden(smp5);
svg(smp6, (cx, cy - 40), "asset:svg/flags/br.svg", 240); hidden(smp6);
svg(smp7, (cx, cy - 40), "asset:svg/emoji/fire.svg", 220); hidden(smp7);
svg(smp8, (cx, cy - 40), "asset:svg/heroicons/bolt.svg", 200); hue(smp8, 48); hidden(smp8);
svg(smp9, (cx, cy - 40), "asset:svg/flags/in.svg", 240); hidden(smp9);
svg(smp10, (cx, cy - 40), "asset:svg/emoji/star.svg", 220); hidden(smp10);
svg(smp11, (cx, cy - 40), "asset:svg/lucide/globe.svg", 200); hue(smp11, 130); hidden(smp11);
svg(smp12, (cx, cy - 40), "asset:svg/emoji/earth.svg", 220); hidden(smp12);
svg(smp13, (cx, cy - 40), "asset:svg/flags/de.svg", 240); hidden(smp13);
svg(smp14, (cx, cy - 40), "asset:svg/heroicons/beaker.svg", 200); hue(smp14, 300); hidden(smp14);
svg(smp15, (cx, cy - 40), "asset:svg/emoji/heart.svg", 220); hidden(smp15);
text(mark, (cx, cy - 20), "MANIC"); size(mark, 110); color(mark, gold); bold(mark); display(mark); glow(mark, 1.2); hidden(mark);
text(tagl, (cx, cy + 78), "6,404 shapes · one file · all computed");
size(tagl, 26); color(tagl, cyan); display(tagl); hidden(tagl);
// ============================================================================
// THE SPECTACLE (fast, punchy)
// ============================================================================
// 1 — SINGULARITY: everything waits in one blinding point.
seq {
zoom(1.35, 0.01);
show(sw0, 0.3);
par { for i in 0..30 { show(sw{i}, 0.25); } } // a tight rainbow knot at centre
pulse(sw0);
say(sub, "One line…", 0.25);
speak("This… is one line of Manic.");
wait(0.2);
}
// 2 — BIG BANG: the storm explodes outward into a ring.
seq {
say(sub, "…one universe.", 0.25);
speak("From it, a whole universe of vectors explodes into being.");
show(spark, 0.05); burst(spark, 0.7);
par {
for i in 0..30 {
move(sw{i}, (cx + 300*cos(i*12*pi/180), cy + 300*sin(i*12*pi/180)), 0.7, out);
}
zoom(1.0, 0.7, smooth);
}
wait(0.15);
}
// 3 — GALAXY: the ring breathes (turn removed to test formation).
seq {
say(sub, "computed — never drawn.", 0.3);
speak("Import any SVG — an icon, a flag, an emoji — as living geometry. Not a picture. Real shapes you can move and colour.");
par { for i in 0..30 { pulse(sw{i}); } }
wait(0.2);
}
// 4 — THE HEART: dozens of vectors snap into a single shape.
seq {
say(sub, "shape them at will.", 0.3);
speak("Watch thirty of them snap into a single form — placed by pure math.");
par {
for i in 0..30 {
move(sw{i},
(cx + 208*sin(i*pi/15)*sin(i*pi/15)*sin(i*pi/15),
cy - (169*cos(i*pi/15) - 65*cos(2*i*pi/15) - 26*cos(3*i*pi/15) - 13*cos(4*i*pi/15)) + 40),
1.1, smooth);
}
}
par { for i in 0..30 { pulse(sw{i}); } } // the heart beats
wait(0.4);
}
// 4.5 — THE LIBRARY: riffle the catalogue so they see the full range.
seq {
say(sub, "and not just hearts — 6,404 shapes, one line each.", 0.3);
speak("And it's not just hearts. Six thousand four hundred shapes ship inside — every icon, every flag, every emoji — each one line away.");
par { for i in 0..30 { fade(sw{i}, 0.4); } } // clear the heart
stagger(0.2) {
for i in 0..16 { seq { show(smp{i}, 0.14); fade(smp{i}, 0.3); } }
}
wait(0.2);
}
// 5 — CELEBRATION: flags and heroes rain in; confetti pops.
seq {
say(sub, "every shape. every colour. one file.", 0.35);
speak("Faithful colours. Real vectors. Recolour them, move them, explode them — all from one text file.");
show(conf, 0.05); burst(conf, 0.9);
stagger(0.06) {
move(flag0, (cx - 420, cy - 250), 0.7, overshoot); show(flag0, 0.1);
move(hero0, (cx - 500, cy + 250), 0.7, overshoot); show(hero0, 0.1);
move(flag1, (cx - 140, cy - 250), 0.7, overshoot); show(flag1, 0.1);
move(hero1, (cx - 250, cy + 250), 0.7, overshoot); show(hero1, 0.1);
move(flag2, (cx + 140, cy - 250), 0.7, overshoot); show(flag2, 0.1);
move(hero2, (cx + 250, cy + 250), 0.7, overshoot); show(hero2, 0.1);
move(flag3, (cx + 420, cy - 250), 0.7, overshoot); show(flag3, 0.1);
move(hero3, (cx + 500, cy + 250), 0.7, overshoot); show(hero3, 0.1);
}
wait(0.4);
}
// 6 — FINALE: it all bursts apart; a rocket tears through to the title.
seq {
say(sub, "", 0.1);
show(spark, 0.05); burst(spark, 0.8);
par {
fade(flag0, 0.5); fade(flag1, 0.5); fade(flag2, 0.5); fade(flag3, 0.5);
fade(hero0, 0.5); fade(hero1, 0.5); fade(hero2, 0.5); fade(hero3, 0.5);
}
show(rk, 0.2);
move(rk, (cx, cy + 40), 0.7, out);
par { flash(rk, cyan); pulse(rk); }
move(rk, (cx, -160), 0.6, in); // it launches off the top
par { show(mark, 0.5); show(tagl, 0.5); }
speak("This is Manic — turn plain text into vector spectacle. Try it free at eight g wifi dot org slash manic.");
flash(mark, gold);
wait(1.4);
}
clip-window
T1 rectangular clip: clip(id, region) scissors a big imported vector to another entity’s
bounds, so a hollow spotlight window sweeps across a flag and reveals it piece by piece. The
scissor is resolved every frame — move the window and the reveal tracks it. Reveal-through-a-
window · crop-to-panel · region wipe · inset viewport, in one word.
// clip-window.manic — T1 rectangular clip: reveal-through-a-window / crop-to-panel.
//
// `clip(id, region)` scissors an entity (or a whole tagged group) to another
// entity's rectangular bounds. The scissor is resolved every frame, so the window
// FOLLOWS the region — move the window and the reveal tracks it. Here a spotlight
// window sweeps across a big imported vector, revealing it piece by piece.
//
// manic examples/clip-window.manic
title("Clip — reveal through a window");
canvas("16:9");
template("blank");
// a big imported vector that overflows what one window can show
svg(art, (cx, cy), "asset:svg/flags/br.svg", 940);
// the spotlight window — hollow frame so the clipped vector shows through
// (declared BEFORE the clip that references it)
rect(win, (cx - 380, cy), 320, 300); outlined(win); outline(win, fg); stroke(win, 5); glow(win, 0.7);
// clip the whole vector to the window — nothing shows outside it
clip(art, win);
text(cap, (cx, h*0.9), "clip(id, window) — the scissor follows the window");
size(cap, 26); color(cap, dim); display(cap);
// sweep the window across → the flag is revealed only where the window is
seq {
wait(0.4);
move(win, (cx + 380, cy), 2.4, smooth);
wait(0.3);
move(win, (cx, cy), 1.0, smooth);
pulse(win);
wait(0.6);
}
mask-lens
T2 arbitrary-shape mask: mask(id, region) composites a vector through another entity’s
SILHOUETTE — any shape masks any content (render-target multiply). A circular lens glides
across a flag like a magnifier; swap the circle for a star or an imported outline and the
reveal takes that shape. The general cousin of clip (rectangle-only). Lens · porthole ·
spotlight · reveal-through-any-shape, and the mask follows the region every frame.
// mask-lens.manic — T2 arbitrary-shape mask: reveal-through-any-shape.
//
// `mask(id, region)` composites `id` through another entity's SILHOUETTE — any
// shape (circle, polygon, star, an imported svg outline) masks any content. The
// general cousin of `clip` (which is rectangle-only). The mask follows `region`
// every frame, so a circular lens glides across a big vector like a magnifier.
//
// manic examples/mask-lens.manic
title("Mask — reveal through any shape");
canvas("16:9");
template("blank");
// a big imported vector to peer into
svg(art, (cx, cy), "asset:svg/flags/br.svg", 940);
// a circular lens — its shape is the mask (declared BEFORE the mask call)
circle(lens, (cx - 340, cy), 150); outlined(lens); outline(lens, fg); stroke(lens, 5); glow(lens, 0.8);
// mask the whole vector to the lens — art shows only inside the circle
mask(art, lens);
text(cap, (cx, h*0.9), "mask(id, region) — any shape masks any content");
size(cap, 26); color(cap, dim); display(cap);
// glide the lens across → the flag is revealed only inside the moving circle
seq {
wait(0.4);
move(lens, (cx + 340, cy), 2.4, smooth);
wait(0.3);
move(lens, (cx, cy), 1.0, smooth);
pulse(lens);
wait(0.6);
}
motion-graphics-v2
The generic Motion Graphics V2 acceptance scene: one persistent marker carries an attached
label along a path, becomes a declared visual blueprint, releases the label, gathers the
same particles into a ring, and turns the whole arrangement around one shared pivot. Uses
attach, become, and turn with no renderer flags or subject-specific vocabulary.
// motion-graphics-v2.manic — relationships, identity, and a deliberate settle
title("Motion Graphics V2 — Follow, Become, Turn");
canvas("9:16");
template("mono");
watermark(manicMark, (150, 132), "Made With Manic");
creator(me, "@anish2good name=Manic_Motion tagline=Describe_the_relationship yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social cta=Create_yours safe=reels");
socials(me);
text(kicker, (540, 155), "MOTION GRAPHICS V2");
size(kicker, 23); color(kicker, dim); bold(kicker);
text(headline, (540, 228), "Move the idea, not every coordinate");
size(headline, 40); bold(headline);
text(subhead, (540, 292), "attach · become · turn");
size(subhead, 25); color(subhead, dim);
rect(stage, (540, 865), 900, 1020);
color(stage, panel); outline(stage, dim); opacity(stage, 0.78);
// One authored route and one persistent object.
plot(route, (205, 1040), 172, 190, "0.55*sin(1.2*x)+0.18*x", (0, 4.25));
color(route, cyan); stroke(route, 6); untraced(route);
dot(marker, (205, 1040), 12);
color(marker, gold); glow(marker, 1.6);
text(markerLabel, (205, 994), "one persistent object");
size(markerLabel, 24); color(markerLabel, fg); hidden(markerLabel);
// A hidden target is a visual blueprint; it never appears as a second object.
circle(nodeBlueprint, (935, 885), 56);
color(nodeBlueprint, magenta); outlined(nodeBlueprint); stroke(nodeBlueprint, 7);
glow(nodeBlueprint, 1.2); hidden(nodeBlueprint);
// Supporting particles begin offstage, then arrive as an ordered ring.
rect(seedBox, (540, 610), 260, 180); hidden(seedBox);
particles(sparks, seedBox, 14, 7, 42, "random");
color(sparks, cyan); hidden(sparks);
circle(finalOrbit, (935, 885), 138); hidden(finalOrbit);
text(explain, (540, 1325), "The label follows because the relationship is authored.");
size(explain, 26); color(explain, dim); wrap(explain, 800); hidden(explain);
step("attach-and-travel") {
seq {
attach(markerLabel, marker, (0,-46));
par {
show(markerLabel, 0.35);
show(explain, 0.40);
draw(route, 2.8, smooth);
travel(marker, route, 2.8, smooth);
}
wait(0.45);
}
}
step("become-the-next-idea") {
seq {
par {
become(marker, nodeBlueprint, 1.05, smooth);
say(markerLabel, "same id · new visual state", 0.45);
say(explain, "become preserves identity and settles on the exact blueprint.", 0.45);
}
wait(0.50);
}
}
step("release-arrange-turn") {
seq {
attach(markerLabel, none);
par {
fade(markerLabel, 0.35);
fade(route, 0.45);
show(sparks, 0.30);
arrange(sparks, finalOrbit, "ring", 1.15, smooth);
say(explain, "Release the label. Arrange a system. Turn it around one pivot.", 0.45);
}
turn(sparks, marker, 24, 0.65, out);
wait(1.40);
}
}
motion-graphics-v2-story
The advanced composition example: one question travels through a field of facts, its WHY?
label follows, notation rewrites into a pattern, the question becomes a model, and the same
facts arrange and turn as one knowledge system. Combines attach, become, and turn with
to, travel, flow, spin, arrange, wander, rewrite, seq, par, and stagger.
// motion-graphics-v2-story.manic — one idea, one continuous motion story
title("The Journey of an Idea — Motion Graphics V2 Story");
canvas("9:16");
template("mono");
watermark(manicMark, (170, 178), "Made With Manic");
creator(me, "@anish2good name=Manic_Motion tagline=Move_the_idea yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social cta=Create_yours safe=reels");
socials(me);
// ---------------------------------------------------------------------------
// ONE PERSISTENT STAGE
// The scene never cuts away. Each act reuses what the previous act established.
// ---------------------------------------------------------------------------
text(kicker, (540, 184), "A MOTION GRAPHICS STORY");
size(kicker, 22); color(kicker, dim); bold(kicker); hidden(kicker);
text(headline, (540, 248), "How an idea learns to move");
size(headline, 42); bold(headline); hidden(headline);
text(chapter, (540, 316), "ACT 1 · FOLLOW ATTENTION");
size(chapter, 24); color(chapter, cyan); bold(chapter); hidden(chapter);
rect(stage, (540, 850), 900, 990);
color(stage, panel); outline(stage, dim); opacity(stage, 0.76);
text(narration, (540, 1400), "A question appears before the answer does.");
size(narration, 28); color(narration, fg); wrap(narration, 820); hidden(narration);
text(principle, (540, 1495), "Motion should guide the eye, preserve meaning, then settle.");
size(principle, 22); color(principle, dim); wrap(principle, 820); hidden(principle);
// ---------------------------------------------------------------------------
// ACT 1 — CURIOSITY TRAVELS; ITS CONTEXT FOLLOWS
// ---------------------------------------------------------------------------
spline(questionPath, (175, 1080), (290, 980), (385, 1060), (490, 900), (540, 790));
color(questionPath, cyan); stroke(questionPath, 5); dashed(questionPath, 18, 15);
glow(questionPath, 0.8); untraced(questionPath);
dot(idea, (175, 1080), 12);
color(idea, gold); glow(idea, 1.7); hidden(idea);
text(focusLabel, (175, 1024), "WHY?");
size(focusLabel, 29); color(focusLabel, gold); bold(focusLabel); hidden(focusLabel);
// Facts begin as visual noise. They persist through every later arrangement.
rect(noiseField, (540, 820), 690, 470); hidden(noiseField);
particles(facts, noiseField, 16, 7, 29, "random");
color(facts, cyan); glow(facts, 0.9); hidden(facts);
for i in 0..16 {
tag(facts.p{i}, knowledgeSystem);
}
// ---------------------------------------------------------------------------
// ACT 2 — THE SAME QUESTION BECOMES A MODEL
// ---------------------------------------------------------------------------
equation(work, (540, 1195), `1+2+\cdots+n=?`, 43);
color(work, fg); hidden(work);
// Hidden entities are visual blueprints. `idea` adopts this exact final state.
rect(modelBlueprint, (540, 790), 116, 116);
outlined(modelBlueprint); outline(modelBlueprint, magenta); stroke(modelBlueprint, 7);
rot(modelBlueprint, 45); glow(modelBlueprint, 1.5); hidden(modelBlueprint);
circle(knowledgeOrbit, (540, 790), 250); hidden(knowledgeOrbit);
text(observe, (540, 492), "OBSERVE");
text(connect, (842, 790), "CONNECT");
text(testIdea, (540, 1088), "TEST");
text(explain, (238, 790), "EXPLAIN");
size(observe, 20); size(connect, 20); size(testIdea, 20); size(explain, 20);
color(observe, dim); color(connect, dim); color(testIdea, dim); color(explain, dim);
bold(observe); bold(connect); bold(testIdea); bold(explain);
hidden(observe); hidden(connect); hidden(testIdea); hidden(explain);
tag(observe, knowledgeSystem); tag(connect, knowledgeSystem);
tag(testIdea, knowledgeSystem); tag(explain, knowledgeSystem);
// ---------------------------------------------------------------------------
// ACT 3 — THE MODEL BECOMES A COORDINATED SYSTEM
// ---------------------------------------------------------------------------
text(systemLabel, (540, 648), "ONE IDEA · MANY RELATIONSHIPS");
size(systemLabel, 22); color(systemLabel, magenta); bold(systemLabel); hidden(systemLabel);
text(finalLine, (540, 1320), "Don't animate layers. Animate the relationship.");
size(finalLine, 30); color(finalLine, fg); bold(finalLine); wrap(finalLine, 820); hidden(finalLine);
// ---------------------------------------------------------------------------
// TIMELINE — seq tells the story; par and stagger control the viewing rhythm.
// ---------------------------------------------------------------------------
step("a-question-pulls-attention") {
seq {
par {
show(kicker, 0.35);
show(headline, 0.55);
show(chapter, 0.40);
show(narration, 0.45);
show(principle, 0.45);
show(idea, 0.30);
}
// Stagger turns a cloud into a readable arrival instead of one visual shock.
stagger(0.035) {
for i in 0..16 {
show(facts.p{i}, 0.22);
}
}
attach(focusLabel, idea, (0,-56));
par {
show(focusLabel, 0.30);
draw(questionPath, 2.65, smooth);
travel(idea, questionPath, 2.65, smooth);
flow(questionPath, 1.45);
wander(facts, 2.65);
}
par {
to(idea, scale, 1.55, 0.45, out);
say(narration, "Attention follows the question while the background stays alive.", 0.45);
}
wait(0.55);
}
}
step("the-question-becomes-a-model") {
seq {
attach(focusLabel, none);
par {
fade(focusLabel, 0.30);
fade(questionPath, 0.45);
say(chapter, "ACT 2 · PRESERVE THE IDEA", 0.35);
say(narration, "The question changes form, but it never loses its identity.", 0.45);
show(work, 0.40);
}
// Existing notation moves only where the mathematical meaning changes.
rewrite(work, `1+2+\cdots+n=\frac{n(n+1)}{2}`, 1.00, smooth);
par {
become(idea, modelBlueprint, 1.05, smooth);
arrange(facts, knowledgeOrbit, "ring", 1.20, smooth);
say(principle, "become keeps one id · arrange keeps every particle", 0.45);
}
// spin is local: the new model turns around its own centre.
spin(idea, 90, 0.65, out);
stagger(0.10) {
show(observe, 0.30);
show(connect, 0.30);
show(testIdea, 0.30);
show(explain, 0.30);
}
wait(0.55);
}
}
step("relationships-move-as-one") {
seq {
attach(systemLabel, idea, (0,-142));
par {
show(systemLabel, 0.35);
say(chapter, "ACT 3 · MOVE THE SYSTEM", 0.35);
say(narration, "Now every part can move together without losing the structure.", 0.45);
show(finalLine, 0.45);
}
// turn is relational: particles and labels share one pivot and one motion.
par {
turn(knowledgeSystem, idea, 45, 1.20, smooth);
spin(idea, -135, 1.20, smooth);
to(facts, color, lime, 1.20, smooth);
rewrite(work, `\text{question}\;\longrightarrow\;\text{model}\;\longrightarrow\;\text{insight}`, 1.20, smooth);
}
par {
pulse(idea, 0.70);
say(principle, "FOLLOW · TRANSFORM · COORDINATE · SETTLE", 0.45);
}
wait(1.80);
}
}
reactive-math-journey
A playful vertical journey from 1+1 in Class 1 through fractions, algebra,
geometry, calculus, linear algebra, probability and Fourier analysis to a PhD-level
functional integral — then back to the curiosity that started it all.
// ============================================================================
// reactive-math-journey.manic — from Class 1 counting to PhD curiosity
// ----------------------------------------------------------------------------
// One equation id grows with the learner. `rewrite` keeps the journey continuous
// while a progress marker climbs through school, university and research.
// The ending returns to 1+1: advanced mathematics keeps the original curiosity.
// ============================================================================
title("From 1 + 1 to a PhD in Mathematics");
canvas("9:16");
template("shorts");
watermark(manicMark, (w*0.895-100, h*0.075+24), "Made With Manic");
creator(me, "@anish2good name=Math_Journey tagline=Stay_curious yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social cta=Keep_learning safe=reels");
socials(me);
text(kicker, (540, 135), "THE JOURNEY OF MATHEMATICS");
size(kicker, 23); color(kicker, dim); bold(kicker); hidden(kicker);
text(headline, (540, 225), "From 1 + 1 to a PhD");
size(headline, 47); color(headline, fg); bold(headline); hidden(headline);
text(subhead, (540, 292), "Same curiosity. Bigger questions.");
size(subhead, 25); color(subhead, cyan); bold(subhead); hidden(subhead);
// The learning stage leaves a slim left lane for the climbing progress marker.
rect(stage, (610, 790), 820, 760);
color(stage, panel); outline(stage, dim); opacity(stage, 0.78);
line(rail, (135, 1135), (135, 535));
color(rail, cyan); stroke(rail, 3); dashed(rail, 13, 10); opacity(rail, 0.35);
untraced(rail);
for i in 0..13 {
dot(milestone{i}, (135, 1135 - i*50), 5);
color(milestone{i}, dim); opacity(milestone{i}, 0.55);
}
dot(learner, (135, 1135), 12);
color(learner, cyan); glow(learner, 2.2); hidden(learner);
text(level, (610, 495), "CLASS 1 · COUNTING");
size(level, 25); color(level, cyan); bold(level); hidden(level);
equation(work, (610, 775), `1+1=2`, 58);
hidden(work);
text(note, (610, 1035), "Two apples. One idea. A lifetime of questions begins.");
size(note, 27); color(note, dim); wrap(note, 700); hidden(note);
text(altitude, (540, 1265), "SCHOOL → UNIVERSITY → RESEARCH");
size(altitude, 22); color(altitude, dim); bold(altitude); hidden(altitude);
// ---------------------------------------------------------------------------
// TIMELINE
// ---------------------------------------------------------------------------
par {
show(kicker, 0.40); show(headline, 0.55); show(subhead, 0.55);
show(level, 0.45); show(note, 0.45); show(altitude, 0.45);
draw(rail, 0.90); show(learner, 0.45);
}
show(work, 0.55);
wait(0.85);
// Class 2 — repeated addition discovers multiplication.
par {
say(level, "CLASS 2 · MULTIPLICATION", 0.35);
say(note, "Addition gets impatient and invents a shortcut.", 0.35);
move(learner, (135, 1085), 0.45, smooth);
}
rewrite(work, `3+3+3+3=12`, 0.80, smooth);
wait(0.35);
rewrite(work, `4\times3=\textcolor{lime}{12}`, 0.80, smooth);
wait(0.65);
// Class 5 — pieces become numbers too.
par {
say(level, "CLASS 5 · FRACTIONS", 0.35);
say(note, "Now even pieces of a whole can be added exactly.", 0.35);
move(learner, (135, 1035), 0.45, smooth);
}
rewrite(work, `\frac{1}{2}+\frac{1}{3}=\textcolor{lime}{\frac{5}{6}}`, 0.90, smooth);
wait(0.70);
// Class 7 — the mystery number gets a name.
par {
say(level, "CLASS 7 · ALGEBRA", 0.35);
say(note, "A blank box becomes x — and arithmetic starts telling stories.", 0.35);
move(learner, (135, 985), 0.45, smooth);
}
rewrite(work, `x+3=7`, 0.80, smooth);
wait(0.30);
rewrite(work, `x=\textcolor{lime}{4}`, 0.75, smooth);
wait(0.65);
// Class 8 — shapes reveal equations hidden inside them.
par {
say(level, "CLASS 8 · GEOMETRY", 0.35);
say(note, "A right triangle quietly connects three squares.", 0.35);
move(learner, (135, 935), 0.45, smooth);
}
rewrite(work, `a^2+b^2=\textcolor{cyan}{c^2}`, 0.85, smooth);
wait(0.70);
// Class 10 — one formula solves every quadratic.
par {
say(level, "CLASS 10 · QUADRATICS", 0.35);
say(note, "The unknown now has two possible futures.", 0.35);
move(learner, (135, 885), 0.45, smooth);
}
rewrite(work, `ax^2+bx+c=0`, 0.85, smooth);
wait(0.30);
rewrite(work, `x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}`, 1.00, smooth);
wait(0.75);
// Class 11 — circles turn into identities.
par {
say(level, "CLASS 11 · TRIGONOMETRY", 0.35);
say(note, "Sine and cosine travel differently, but always return to one.", 0.35);
move(learner, (135, 835), 0.45, smooth);
}
rewrite(work, `\sin^2\theta+\cos^2\theta=\textcolor{lime}{1}`, 0.90, smooth);
wait(0.70);
// Class 12 — mathematics learns to describe change itself.
par {
say(level, "CLASS 12 · CALCULUS", 0.35);
say(note, "Not just where things are — how fast they are changing.", 0.35);
move(learner, (135, 785), 0.45, smooth);
}
rewrite(work, `\frac{d}{dx}x^n=\textcolor{cyan}{nx^{n-1}}`, 0.95, smooth);
wait(0.75);
// University — numbers become transformations and uncertain beliefs.
par {
say(level, "UNIVERSITY · LINEAR ALGEBRA", 0.35);
say(note, "Some directions do not turn — they only stretch.", 0.35);
move(learner, (135, 735), 0.45, smooth);
}
rewrite(work, `A\vec v=\textcolor{magenta}{\lambda}\vec v`, 0.90, smooth);
wait(0.70);
par {
say(level, "UNIVERSITY · PROBABILITY", 0.35);
say(note, "Evidence arrives, and a belief learns how to update.", 0.35);
move(learner, (135, 685), 0.45, smooth);
}
rewrite(work, `P(A\mid B)=\frac{P(B\mid A)P(A)}{P(B)}`, 0.95, smooth);
wait(0.75);
// Graduate study — equations evolve, then decompose into waves.
par {
say(level, "GRADUATE · DIFFERENTIAL EQUATIONS", 0.35);
say(note, "A tiny law of change predicts an entire future.", 0.35);
move(learner, (135, 635), 0.45, smooth);
}
rewrite(work, `\frac{dy}{dt}=ky`, 0.85, smooth);
wait(0.30);
rewrite(work, `y(t)=y_0e^{kt}`, 0.85, smooth);
wait(0.70);
par {
say(level, "GRADUATE · FOURIER ANALYSIS", 0.35);
say(note, "A complicated signal turns out to be a choir of simple waves.", 0.35);
move(learner, (135, 585), 0.45, smooth);
}
rewrite(work, `f(x)=\sum_{n=-\infty}^{\infty}c_ne^{inx}`, 0.95, smooth);
wait(0.75);
// PhD — the symbols are advanced; the habit is still “what if?”
par {
say(level, "PhD · THE EDGE OF WHAT WE KNOW", 0.40);
say(note, "The integral now ranges over every possible field configuration.", 0.40);
move(learner, (135, 535), 0.55, overshoot);
}
rewrite(work, `\mathcal{Z}=\int\mathcal{D}\phi\,e^{-S[\phi]}`, 1.05, smooth);
wait(1.00);
// A loopable ending: the notation grew, but the first question never left.
say(level, "THE REAL DEGREE · STAYING CURIOUS", 0.40);
say(note, "After all that mathematics, the best researchers still ask: why?", 0.40);
rewrite(work, `1+1=\textcolor{lime}{2}`, 1.00, smooth);
pulse(learner, 0.65);
wait(1.80);
reactive-math-notation
One Reels-ready stage exercises structured LaTeX across thirteen notation worlds: algebra, calculus, limits, trigonometry, logic, sums/products, physics, chemistry, biology, probability, matrices/vectors, mixed prose/math, and creator notation.
// ============================================================================
// reactive-math-notation.manic — one Reels-ready stage, thirteen notation worlds
// ----------------------------------------------------------------------------
// `rewrite` is deliberately domain-neutral. The same persistent equation moves
// through algebra, calculus, logic, physics, chemistry, biology, probability
// and linear algebra;
// only changed RaTeX parts leave or enter. Mixed prose + inline math uses normal
// `text`, and the last scene proves creator-defined notation needs no new verb.
// ============================================================================
title("Reactive Mathematics — One Language, Every Notation");
canvas("9:16");
template("shorts");
watermark(manicMark, (w*0.895-100, h*0.075+24), "Made With Manic");
creator(me, "@anish2good name=Notation_Lab tagline=Every_symbol_can_move yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social cta=Create_your_own safe=reels");
socials(me);
text(kicker, (540, 145), "ONE EQUATION · THIRTEEN SCIENTIFIC WORLDS");
size(kicker, 23); color(kicker, dim); bold(kicker); hidden(kicker);
text(headline, (540, 235), "If LaTeX can say it, Manic can move it");
size(headline, 39); color(headline, fg); bold(headline); hidden(headline);
rect(stage, (540, 755), 940, 760);
color(stage, panel); outline(stage, dim); opacity(stage, 0.72); hidden(stage);
text(topic, (540, 445), "01 · ALGEBRAIC REARRANGEMENT");
size(topic, 23); color(topic, cyan); bold(topic); hidden(topic);
equation(work, (540, 730), `2(x+3)=14`, 52);
hidden(work);
text(note, (540, 1040), "Keep the structure. Move only the mathematical change.");
size(note, 25); color(note, dim); wrap(note, 800); hidden(note);
// A normal text entity may freely mix prose and several inline formulas.
text(mixed, (540, 735), `Energy $E=mc^2$ uses mass $m$ and light speed $c$.`);
size(mixed, 31); color(mixed, fg); wrap(mixed, 780); hidden(mixed);
text(scope, (540, 1245), "RaTeX accuracy · local motion · one stable layout");
size(scope, 22); color(scope, dim); bold(scope); hidden(scope);
// A subtle dashed construction line also demonstrates that `dashed` belongs
// to Manic itself — plots and all other path-like entities can use it.
line(rule, (215, 1155), (865, 1155));
color(rule, cyan); stroke(rule, 2); dashed(rule, 12, 9); opacity(rule, 0.35);
untraced(rule);
// ---------------------------------------------------------------------------
// TIMELINE — every chapter reuses `work`; nothing is manually repositioned.
// ---------------------------------------------------------------------------
par {
show(kicker, 0.45); show(headline, 0.55); show(stage, 0.50);
show(topic, 0.45); show(note, 0.45); show(scope, 0.45); draw(rule, 0.85);
}
show(work, 0.55);
wait(0.70);
rewrite(work, `2x=8\quad\Rightarrow\quad \textcolor{lime}{x=4}`, 0.85, smooth);
wait(0.75);
say(topic, "02 · INTEGRALS & DERIVATIVES", 0.35);
say(note, "The Fundamental Theorem becomes a change of state, not a scene cut.", 0.35);
rewrite(work, `F(x)=\int_0^x t^2\,dt`, 0.80, smooth);
wait(0.45);
rewrite(work, `F'(x)=\frac{d}{dx}\int_0^x t^2\,dt=\textcolor{cyan}{x^2}`, 0.95, smooth);
wait(0.70);
say(topic, "03 · FRACTIONS, ROOTS, POWERS & LIMITS", 0.35);
say(note, "Nested notation is still one screen-aware equation.", 0.35);
rewrite(work, `x^2+\sqrt{x}+\frac{1}{x}`, 0.80, smooth);
wait(0.40);
rewrite(work, `\lim_{x\to0}\frac{\sqrt{1+x}-1}{x}=\frac{1}{2}`, 0.95, smooth);
wait(0.70);
say(topic, "04 · TRIGONOMETRIC IDENTITIES", 0.35);
say(note, "The unchanged terms stay; only the conclusion arrives.", 0.35);
rewrite(work, `\sin^2\theta+\cos^2\theta`, 0.80, smooth);
wait(0.40);
rewrite(work, `\sin^2\theta+\cos^2\theta=\textcolor{lime}{1}`, 0.85, smooth);
wait(0.70);
say(topic, "05 · SET NOTATION & LOGIC", 0.35);
say(note, "Membership, intersections and propositions share the same engine.", 0.35);
rewrite(work, `x\in A\cap B`, 0.80, smooth);
wait(0.40);
rewrite(work, `(x\in A)\land(x\in B)`, 0.85, smooth);
wait(0.70);
say(topic, "06 · SUMMATIONS & PRODUCTS", 0.35);
say(note, "Large operators retain their limits and baseline alignment.", 0.35);
rewrite(work, `\sum_{k=1}^{n}k`, 0.80, smooth);
wait(0.40);
rewrite(work, `\prod_{k=1}^{n}k=\textcolor{magenta}{n!}`, 0.85, smooth);
wait(0.70);
say(topic, "07 · PHYSICS FORMULAS & UNITS", 0.35);
say(note, "Symbols and dimensional units can live in one exact expression.", 0.35);
rewrite(work, `F=ma`, 0.80, smooth);
wait(0.40);
rewrite(work, `[F]=\mathrm{kg}\cdot\mathrm{m}\cdot\mathrm{s}^{-2}`, 0.90, smooth);
wait(0.70);
say(topic, "08 · CHEMISTRY · BALANCED REACTIONS", 0.35);
say(note, "Keep every molecule; introduce only the coefficients that balance atoms.", 0.35);
rewrite(work, `H_2+O_2\rightarrow H_2O`, 0.80, smooth);
wait(0.40);
rewrite(work, `\textcolor{cyan}{2}H_2+O_2\rightarrow\textcolor{cyan}{2}H_2O`, 0.90, smooth);
wait(0.70);
say(topic, "09 · BIOLOGY · HARDY–WEINBERG", 0.35);
say(note, "A population model grows from allele frequencies into genotype frequencies.", 0.35);
rewrite(work, `p+q=1`, 0.80, smooth);
wait(0.40);
rewrite(work, `p^2+2pq+q^2=\textcolor{lime}{1}`, 0.90, smooth);
wait(0.70);
say(topic, "10 · PROBABILITY EXPRESSIONS", 0.35);
say(note, "Conditioning expands into Bayes' rule without replacing the stage.", 0.35);
rewrite(work, `P(A\mid B)`, 0.80, smooth);
wait(0.40);
rewrite(work, `P(A\mid B)=\frac{P(B\mid A)P(A)}{P(B)}`, 0.95, smooth);
wait(0.70);
say(topic, "11 · MATRICES & VECTORS", 0.35);
say(note, "Rows, columns, brackets and vector marks remain genuine LaTeX.", 0.35);
rewrite(work, `\vec v=\begin{bmatrix}1\\2\end{bmatrix}`, 0.85, smooth);
wait(0.40);
rewrite(work, `A\vec v=\begin{bmatrix}a&b\\c&d\end{bmatrix}\begin{bmatrix}1\\2\end{bmatrix}`, 1.00, smooth);
wait(0.75);
say(topic, "12 · TEXT MIXED WITH MATHEMATICS", 0.35);
say(note, "Ordinary prose can carry multiple inline formulas naturally.", 0.35);
fade(work, 0.35);
show(mixed, 0.55);
wait(1.25);
fade(mixed, 0.35);
say(topic, "13 · YOUR OWN NOTATION", 0.35);
say(note, "Define the meaning in your story; Manic preserves the supported notation.", 0.35);
rewrite(work, `\mathcal{R}_{\star}(x)\equiv x^2+1`, 0.90, smooth);
wait(0.45);
rewrite(work, `\mathcal{R}_{\star}(2)=\textcolor{lime}{5}`, 0.85, smooth);
wait(1.20);
say(topic, "ONE REACTIVE LANGUAGE", 0.40);
say(note, "Math, physics, logic — creators decide what comes next.", 0.40);
pulse(work, 0.70);
wait(1.80);
quadratic-formula-continuity
The quadratic formula by completing the square with one persistent LaTeX equation.
Each authored rewrite retains unchanged symbols, moves reused terms, and introduces
only the new notation — the acceptance benchmark for structured formula motion.
// quadratic-formula-continuity.manic — structured LaTeX rewrite benchmark
//
// The author supplies correct mathematical states. `rewrite` does not solve
// algebra: it keeps equal RaTeX parts alive, moves them to their new roles,
// introduces only new notation, and settles on the exact target formula.
title("The Quadratic Formula by Completing the Square");
canvas("16:9");
template("plain");
watermark(manicMark, (1135, 30), "Made With Manic");
text(kicker, (cx, 72), "COMPLETING THE SQUARE · ONE CONTINUOUS EQUATION");
size(kicker, 18); color(kicker, dim); bold(kicker); hidden(kicker);
text(guide, (cx, 620), "Stable symbols stay. Only the mathematical change moves.");
size(guide, 19); color(guide, dim); hidden(guide);
equation(
work,
(cx, 330),
`\textcolor{orange}{a}x^2+\textcolor{cyan}{b}x+\textcolor{lime}{c}=0`,
48
);
hidden(work);
show(kicker, 0.45);
show(guide, 0.45);
show(work, 0.65);
wait(1.20);
// Divide through by a.
rewrite(
work,
`x^2+\frac{\textcolor{cyan}{b}}{\textcolor{orange}{a}}x+\frac{\textcolor{lime}{c}}{\textcolor{orange}{a}}=0`,
1.00,
smooth
);
wait(1.00);
// Move c/a across the equality.
rewrite(
work,
`x^2+\frac{\textcolor{cyan}{b}}{\textcolor{orange}{a}}x=-\frac{\textcolor{lime}{c}}{\textcolor{orange}{a}}`,
0.95,
smooth
);
wait(0.95);
// Split the middle coefficient into two equal halves.
rewrite(
work,
`x^2+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}x+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}x=-\frac{\textcolor{lime}{c}}{\textcolor{orange}{a}}`,
1.05,
smooth
);
wait(0.95);
// Add the completing-square term to both sides.
rewrite(
work,
`x^2+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}x+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}x+\left(\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}\right)^2=\left(\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}\right)^2-\frac{\textcolor{lime}{c}}{\textcolor{orange}{a}}`,
1.10,
smooth
);
wait(1.05);
// Simplify the right side locally.
rewrite(
work,
`x^2+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}x+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}x+\left(\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}\right)^2=\frac{\textcolor{cyan}{b}^2-4\textcolor{orange}{a}\textcolor{lime}{c}}{4\textcolor{orange}{a}^2}`,
1.00,
smooth
);
wait(1.00);
// Expose the repeated factor, then contract it into a square.
rewrite(
work,
`x\left(x+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}\right)+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}\left(x+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}\right)=\frac{\textcolor{cyan}{b}^2-4\textcolor{orange}{a}\textcolor{lime}{c}}{4\textcolor{orange}{a}^2}`,
1.05,
smooth
);
wait(0.90);
rewrite(
work,
`\left(x+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}\right)^2=\frac{\textcolor{cyan}{b}^2-4\textcolor{orange}{a}\textcolor{lime}{c}}{4\textcolor{orange}{a}^2}`,
0.95,
smooth
);
wait(1.00);
// Take square roots.
rewrite(
work,
`x+\frac{\textcolor{cyan}{b}}{2\textcolor{orange}{a}}=\pm\frac{\sqrt{\textcolor{cyan}{b}^2-4\textcolor{orange}{a}\textcolor{lime}{c}}}{2\textcolor{orange}{a}}`,
1.05,
smooth
);
wait(1.10);
// Isolate x. The fraction moves across; the discriminant remains itself.
rewrite(
work,
`x=\frac{-\textcolor{cyan}{b}\pm\sqrt{\textcolor{cyan}{b}^2-4\textcolor{orange}{a}\textcolor{lime}{c}}}{2\textcolor{orange}{a}}`,
1.10,
smooth
);
pulse(work, 0.65);
wait(2.50);
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);
morph-icons
Morph imported svg() vectors: one emoji reshapes into another and back. A bare svg tag
resolves to its dominant path, filled Regions sample along their outer ring, and the two
outlines are winding-matched + rotated for a clean shape-to-shape blend — no tangle through
the centre. Paired with a colour tween so the heart warms to gold as it becomes a star.
// morph-icons.manic — morph imported SVG vectors from one shape into another.
//
// `morph(a, b)` samples both outlines and blends `a` into `b`'s shape; drive it
// with `to(a, morph, 1, dur)`. It now works on imported `svg()` art: a bare svg
// tag resolves to its dominant path, filled Regions sample along their outer ring,
// and the two loops are winding-matched + rotated for a clean shape-to-shape blend
// (no tangle through the centre). Here one emoji reshapes into another and back.
//
// manic examples/morph-icons.manic
title("Morph — imported vectors reshape");
canvas("16:9");
template("blank");
svg(heart, (cx, cy), "asset:svg/emoji/heart.svg", 260);
svg(star, (cx, cy), "asset:svg/emoji/star.svg", 260); hidden(star);
text(cap, (cx, h*0.86), "morph(heart, star) — one imported icon reshapes into another");
size(cap, 24); color(cap, dim); display(cap);
// set the heart up to morph into the star's outline
morph(heart, star);
seq {
wait(0.5);
par { to(heart, morph, 1, 1.6, smooth); to(heart, color, gold, 1.6); }
wait(0.5);
par { to(heart, morph, 0, 1.6, smooth); to(heart, color, magenta, 1.6); }
wait(0.5);
}
shapes-howto
SHAPECRAFT — Manic shape power in one film: outlined vs filled (the axes trap),
the cast with particles/travel, the full palette that becomes squares then triangles,
spin-wound morph, then one coral disc that splits into four independent colours,
reunites as one fill, and moves — beside two-colour boolean regions (A−B / B−A / A∩B).
Docs-in-motion for the Shapes chapter.
// ============================================================================
// shapes-howto.manic — SHAPECRAFT
// A power demo that also teaches the rules:
// fills hide constructions · outlined reveals · dashed rings for contacts
// circle/rect/polygon/sector fill by default · morph / become change identity
//
// manic examples/shapes-howto.manic
// manic examples/shapes-howto.manic --record out --preset studio
// ============================================================================
title("Shapecraft — Manic Shape Power");
canvas("16:9");
template("blank");
text(cap, (cx, h - 34), ""); size(cap, 22); color(cap, dim); display(cap);
text(head, (cx, 58), ""); size(head, 32); color(head, gold); display(head); bold(head);
// ---- stage energy ----
circle(well, (cx, cy), 420);
filled(well); gradient(well, panel, void, indigo, radial); opacity(well, 0.7);
spline(orbit, (180, 520), (340, 220), (640, 160), (940, 220), (1100, 520));
stroke(orbit, 2.5); gradient(orbit, dim, violet, teal, "curvature");
untraced(orbit); hidden(orbit); opacity(orbit, 0.55);
// ---- hero: one persistent outlined circle ----
circle(hero, (cx, cy), 120);
outlined(hero); color(hero, cyan); stroke(hero, 6); glow(hero, 1.4);
gradient(hero, cyan, mint, teal, 45);
untraced(hero); hidden(hero);
// fill veil — fades over the hero to show the trap without destroying morph
circle(veil, (cx, cy), 120);
filled(veil); color(veil, indigo); opacity(veil, 0.92); hidden(veil); z(veil, 5);
rect(asSquare, (cx, cy), 220, 220); hidden(asSquare);
polygon(asTri, (cx, 250), (cx - 140, 520), (cx + 140, 520)); hidden(asTri);
sector(asPie, (cx, cy), 130, -30, 300); color(asPie, magenta); hidden(asPie);
polygon(asDiamond, (cx, cy - 130), (cx + 130, cy), (cx, cy + 130), (cx - 130, cy));
color(asDiamond, gold); hidden(asDiamond);
circle(finalForm, (cx, cy), 130);
outlined(finalForm); color(finalForm, cyan); stroke(finalForm, 6);
gradient(finalForm, cyan, mint, violet, 90); glow(finalForm, 1.6); hidden(finalForm);
morph(hero, asSquare, 180); // spin-wound morph ready
text(heroTag, (cx, cy - 170), "one persistent id");
size(heroTag, 22); color(heroTag, fg); hidden(heroTag);
// ---- axes for the fill-trap punch ----
line(axX, (cx - 200, cy), (cx + 200, cy));
color(axX, dim); stroke(axX, 2); untraced(axX); hidden(axX);
line(axY, (cx, cy - 160), (cx, cy + 160));
color(axY, dim); stroke(axY, 2); untraced(axY); hidden(axY);
// ---- the cast (draw-on parade, then orbit travel) ----
circle(castC, (cx - 280, 280), 48);
outlined(castC); color(castC, teal); stroke(castC, 3.5); untraced(castC); hidden(castC);
rect(castR, (cx - 100, 250), 90, 70);
outlined(castR); color(castR, coral); stroke(castR, 3.5); untraced(castR); hidden(castR);
polygon(castP, (cx + 80, 230), (cx + 30, 310), (cx + 130, 310));
outlined(castP); color(castP, violet); stroke(castP, 3.5); untraced(castP); hidden(castP);
sector(castS, (cx + 260, 280), 52, -40, 240);
outlined(castS); color(castS, gold); stroke(castS, 3.5); untraced(castS); hidden(castS);
arc(castA, (cx - 220, 480), 50, -20, 250);
color(castA, mint); stroke(castA, 4); untraced(castA); hidden(castA);
arrow(castV, (cx + 40, 500), (cx + 180, 430));
color(castV, magenta); stroke(castV, 4); untraced(castV); hidden(castV);
line(castL, (cx + 240, 500), (cx + 340, 430));
color(castL, lime); stroke(castL, 4); untraced(castL); hidden(castL);
// sparks that ring the hero, then settle
rect(seed, (cx, cy), 60, 60); hidden(seed);
particles(sparks, seed, 12, 6, 19, "random");
color(sparks, cyan); glow(sparks, 0.5); hidden(sparks);
circle(orbitRing, (cx, cy), 230); hidden(orbitRing);
// ---- palette chips (fill OK — colour IS the subject) ----
let py = 400;
circle(chip0, (140, py), 26); filled(chip0); color(chip0, fg); hidden(chip0);
circle(chip1, (220, py), 26); filled(chip1); color(chip1, cyan); hidden(chip1);
circle(chip2, (300, py), 26); filled(chip2); color(chip2, magenta); hidden(chip2);
circle(chip3, (380, py), 26); filled(chip3); color(chip3, lime); hidden(chip3);
circle(chip4, (460, py), 26); filled(chip4); color(chip4, gold); hidden(chip4);
circle(chip5, (540, py), 26); filled(chip5); color(chip5, red); hidden(chip5);
circle(chip6, (620, py), 26); filled(chip6); color(chip6, orange); hidden(chip6);
circle(chip7, (700, py), 26); filled(chip7); color(chip7, blue); hidden(chip7);
circle(chip8, (780, py), 26); filled(chip8); color(chip8, teal); hidden(chip8);
circle(chip9, (860, py), 26); filled(chip9); color(chip9, violet); hidden(chip9);
circle(chip10, (940, py), 26); filled(chip10); color(chip10, coral); hidden(chip10);
circle(chip11, (1020,py), 26); filled(chip11); color(chip11, indigo); hidden(chip11);
circle(chip12, (1100,py), 26); filled(chip12); color(chip12, mint); hidden(chip12);
circle(chip13, (1180,py), 26); filled(chip13); color(chip13, dim); hidden(chip13);
rect(sq0, (140, py), 48, 48); color(sq0, fg); filled(sq0); hidden(sq0);
rect(sq1, (220, py), 48, 48); color(sq1, cyan); filled(sq1); hidden(sq1);
rect(sq2, (300, py), 48, 48); color(sq2, magenta); filled(sq2); hidden(sq2);
rect(sq3, (380, py), 48, 48); color(sq3, lime); filled(sq3); hidden(sq3);
rect(sq4, (460, py), 48, 48); color(sq4, gold); filled(sq4); hidden(sq4);
rect(sq5, (540, py), 48, 48); color(sq5, red); filled(sq5); hidden(sq5);
rect(sq6, (620, py), 48, 48); color(sq6, orange); filled(sq6); hidden(sq6);
rect(sq7, (700, py), 48, 48); color(sq7, blue); filled(sq7); hidden(sq7);
rect(sq8, (780, py), 48, 48); color(sq8, teal); filled(sq8); hidden(sq8);
rect(sq9, (860, py), 48, 48); color(sq9, violet); filled(sq9); hidden(sq9);
rect(sq10, (940, py), 48, 48); color(sq10, coral); filled(sq10); hidden(sq10);
rect(sq11, (1020,py), 48, 48); color(sq11, indigo); filled(sq11); hidden(sq11);
rect(sq12, (1100,py), 48, 48); color(sq12, mint); filled(sq12); hidden(sq12);
rect(sq13, (1180,py), 48, 48); color(sq13, dim); filled(sq13); hidden(sq13);
polygon(tri0, (140, py-22), (114, py+22), (166, py+22)); color(tri0, fg); filled(tri0); hidden(tri0);
polygon(tri1, (220, py-22), (194, py+22), (246, py+22)); color(tri1, cyan); filled(tri1); hidden(tri1);
polygon(tri2, (300, py-22), (274, py+22), (326, py+22)); color(tri2, magenta); filled(tri2); hidden(tri2);
polygon(tri3, (380, py-22), (354, py+22), (406, py+22)); color(tri3, lime); filled(tri3); hidden(tri3);
polygon(tri4, (460, py-22), (434, py+22), (486, py+22)); color(tri4, gold); filled(tri4); hidden(tri4);
polygon(tri5, (540, py-22), (514, py+22), (566, py+22)); color(tri5, red); filled(tri5); hidden(tri5);
polygon(tri6, (620, py-22), (594, py+22), (646, py+22)); color(tri6, orange); filled(tri6); hidden(tri6);
polygon(tri7, (700, py-22), (674, py+22), (726, py+22)); color(tri7, blue); filled(tri7); hidden(tri7);
polygon(tri8, (780, py-22), (754, py+22), (806, py+22)); color(tri8, teal); filled(tri8); hidden(tri8);
polygon(tri9, (860, py-22), (834, py+22), (886, py+22)); color(tri9, violet); filled(tri9); hidden(tri9);
polygon(tri10, (940, py-22), (914, py+22), (966, py+22)); color(tri10, coral); filled(tri10); hidden(tri10);
polygon(tri11, (1020,py-22),(994, py+22),(1046,py+22)); color(tri11, indigo); filled(tri11); hidden(tri11);
polygon(tri12, (1100,py-22),(1074,py+22),(1126,py+22)); color(tri12, mint); filled(tri12); hidden(tri12);
polygon(tri13, (1180,py-22),(1154,py+22),(1206,py+22)); color(tri13, dim); filled(tri13); hidden(tri13);
// ---- CENTER: one filled disc → 4 independent colours → one colour → move ----
circle(disc, (cx, cy), 140);
filled(disc); color(disc, coral); outline(disc, cyan); stroke(disc, 4);
glow(disc, 0.6); hidden(disc);
pie(q, (cx, cy), 140, 4); // q0..q3 — same centre/radius as disc
color(q0, teal); filled(q0); outline(q0, fg); stroke(q0, 2); untraced(q0); hidden(q0);
color(q1, coral); filled(q1); outline(q1, fg); stroke(q1, 2); untraced(q1); hidden(q1);
color(q2, violet); filled(q2); outline(q2, fg); stroke(q2, 2); untraced(q2); hidden(q2);
color(q3, gold); filled(q3); outline(q3, fg); stroke(q3, 2); untraced(q3); hidden(q3);
// reunited whole — one colour, one id that can move
circle(whole, (cx, cy), 140);
filled(whole); color(whole, cyan); outline(whole, mint); stroke(whole, 4);
glow(whole, 0.8); hidden(whole);
// ---- RIGHT: boolean → TWO regions, TWO colours ----
let bx = 1000;
let by = 400;
rect(bBox, (bx - 35, by - 35), 130, 130);
outlined(bBox); outline(bBox, cyan); opacity(bBox, 0.4); hidden(bBox);
circle(bCirc, (bx + 40, by + 20), 78);
outlined(bCirc); outline(bCirc, magenta); opacity(bCirc, 0.4); hidden(bCirc);
difference(onlyBox, bBox, bCirc, teal); hidden(onlyBox); glow(onlyBox, 0.6);
difference(onlyCirc, bCirc, bBox, coral); hidden(onlyCirc); glow(onlyCirc, 0.6);
intersect(bHit, bBox, bCirc, gold); hidden(bHit); glow(bHit, 0.5);
text(labBool, (bx, by + 155), "A−B · B−A · A∩B"); size(labBool, 16); color(labBool, dim); hidden(labBool);
// ---- contact truth ----
line(crossH, (cx - 160, cy), (cx + 160, cy));
color(crossH, dim); stroke(crossH, 2.5); dashed(crossH, 12, 8); untraced(crossH); hidden(crossH);
line(crossV, (cx, cy - 120), (cx, cy + 120));
color(crossV, dim); stroke(crossV, 2.5); dashed(crossV, 12, 8); untraced(crossV); hidden(crossV);
dot(badDot, (cx, cy), 16); color(badDot, coral); glow(badDot, 0.5); hidden(badDot);
circle(goodRing, (cx, cy), 18);
outlined(goodRing); color(goodRing, mint); stroke(goodRing, 2.8); dashed(goodRing, 4, 3);
untraced(goodRing); hidden(goodRing);
// ---- traveler on the orbit ----
dot(rider, (180, 520), 9); color(rider, gold); glow(rider, 1.2); hidden(rider);
// ============================================================================
// SCRIPT — one continuous shapecraft story
// ============================================================================
step("birth") {
seq {
say(head, "SHAPECRAFT", 0.35);
say(cap, "one shape. every identity. the visual stays true.", 0.35);
show(hero, 0.05);
par {
draw(hero, 1.2, smooth);
zoom(1.15, 1.2, smooth);
}
attach(heroTag, hero, (0, -160));
show(heroTag, 0.35);
wait(0.35);
}
}
step("truth") {
seq {
say(head, "outlined — constructions stay readable", 0.3);
say(cap, "axes through the hero: the rim never hides the truth.", 0.3);
par {
show(axX, 0.05); show(axY, 0.05);
draw(axX, 0.55); draw(axY, 0.55);
cam((cx, cy), 0.8, smooth); zoom(1.35, 0.8, smooth);
}
pulse(hero);
wait(0.45);
say(head, "filled — the trap", 0.25);
say(cap, "a filled disc sits on the axes and deletes them visually.", 0.25);
show(veil, 0.45);
flash(veil, coral);
wait(0.7);
say(head, "outlined — axes return", 0.25);
say(cap, "outline(id, c) only recolors the rim. outlined drops the fill.", 0.25);
fade(veil, 0.45);
flash(hero, mint);
wait(0.55);
par {
fade(axX, 0.35); fade(axY, 0.35); fade(heroTag, 0.35); fade(hero, 0.35);
cam((cx, cy), 0.7, smooth); zoom(1.0, 0.7, smooth);
}
}
}
step("cast") {
seq {
say(head, "the cast", 0.3);
say(cap, "circle · rect · polygon · sector · arc · line · arrow", 0.3);
show(sparks, 0.25);
arrange(sparks, orbitRing, "ring", 1.0, smooth);
wait(0.2);
stagger(0.1) {
show(castC, 0.05); draw(castC, 0.55);
show(castR, 0.05); draw(castR, 0.55);
show(castP, 0.05); draw(castP, 0.55);
show(castS, 0.05); draw(castS, 0.55);
show(castA, 0.05); draw(castA, 0.55);
show(castV, 0.05); draw(castV, 0.55);
show(castL, 0.05); draw(castL, 0.55);
}
wait(0.2);
show(orbit, 0.05); draw(orbit, 1.0, smooth);
show(rider, 0.2);
travel(rider, orbit, 2.0, smooth);
wait(0.25);
par {
fade(sparks, 0.4);
fade(castC, 0.4); fade(castR, 0.4); fade(castP, 0.4); fade(castS, 0.4);
fade(castA, 0.4); fade(castV, 0.4); fade(castL, 0.4);
fade(orbit, 0.4); fade(rider, 0.4);
}
}
}
step("palette") {
seq {
say(head, "the whole palette — then change the shape", 0.3);
say(cap, "filled chips when colour is the subject. become keeps the id.", 0.3);
stagger(0.05) {
show(chip0, 0.22); show(chip1, 0.22); show(chip2, 0.22); show(chip3, 0.22);
show(chip4, 0.22); show(chip5, 0.22); show(chip6, 0.22); show(chip7, 0.22);
show(chip8, 0.22); show(chip9, 0.22); show(chip10, 0.22); show(chip11, 0.22);
show(chip12, 0.22); show(chip13, 0.22);
}
wait(0.35);
say(cap, "become → square", 0.2);
stagger(0.035) {
for i in 0..14 { become(chip{i}, sq{i}, 0.65, smooth); }
}
wait(0.25);
say(cap, "become → triangle", 0.2);
stagger(0.035) {
for i in 0..14 { become(chip{i}, tri{i}, 0.65, smooth); }
}
wait(0.45);
par {
for i in 0..14 { fade(chip{i}, 0.35); }
}
}
}
step("morph") {
seq {
say(head, "morph · become · spin", 0.3);
say(cap, "same hero id — circle winds into a square, then becomes new geometry.", 0.3);
show(hero, 0.3);
to(hero, morph, 1, 1.5, smooth); // → square (with 180° wind)
wait(0.25);
become(hero, asDiamond, 0.95, smooth);
pulse(hero);
wait(0.2);
become(hero, asPie, 0.95, smooth);
spin(hero, 40, 0.7, out);
wait(0.2);
become(hero, asTri, 0.9, smooth);
wait(0.45);
}
}
step("pieces") {
seq {
say(head, "one fill → four fills → one fill → move", 0.3);
say(cap, "a filled disc can split into independent colours, reunite, then travel as one.", 0.3);
fade(hero, 0.3);
// 1 — the coral disc (same look as the fill-trap)
par {
show(axX, 0.05); show(axY, 0.05);
draw(axX, 0.4); draw(axY, 0.4);
cam((cx, cy), 0.6, smooth); zoom(1.25, 0.6, smooth);
}
show(disc, 0.45);
flash(disc, coral);
wait(0.5);
// 2 — cut into 4 independent colours
say(cap, "cut: each quadrant keeps its own fill.", 0.25);
fade(disc, 0.3);
stagger(0.1) {
show(q0, 0.05); draw(q0, 0.45);
show(q1, 0.05); draw(q1, 0.45);
show(q2, 0.05); draw(q2, 0.45);
show(q3, 0.05); draw(q3, 0.45);
}
// explode slightly so the independence is obvious
par {
move(q0, (cx + 22, cy - 22), 0.45, overshoot);
move(q1, (cx - 22, cy - 22), 0.45, overshoot);
move(q2, (cx - 22, cy + 22), 0.45, overshoot);
move(q3, (cx + 22, cy + 22), 0.45, overshoot);
}
wait(0.45);
// 3 — reunite as ONE colour, then ONE circle
say(cap, "reunite: every slice takes one colour, then they become one disc.", 0.25);
par {
recolor(q0, cyan, 0.5);
recolor(q1, cyan, 0.5);
recolor(q2, cyan, 0.5);
recolor(q3, cyan, 0.5);
}
// snap slices back to centre
par {
move(q0, (cx, cy), 0.5, smooth);
move(q1, (cx, cy), 0.5, smooth);
move(q2, (cx, cy), 0.5, smooth);
move(q3, (cx, cy), 0.5, smooth);
}
wait(0.15);
// swap the four pieces for one solid disc (same colour)
par {
fade(q0, 0.3); fade(q1, 0.3); fade(q2, 0.3); fade(q3, 0.3);
show(whole, 0.35);
}
pulse(whole);
wait(0.4);
// 4 — move as one unit
say(cap, "now it moves as a single identity.", 0.25);
par {
fade(axX, 0.35); fade(axY, 0.35);
cam((cx + 120, cy), 0.9, smooth); zoom(1.05, 0.9, smooth);
move(whole, (cx + 260, cy), 1.1, smooth);
}
wait(0.35);
// side boolean still lands on the right as the disc arrives
say(head, "boolean keeps two colours", 0.25);
say(cap, "A−B teal · B−A coral · then A∩B gold — never one muddy fill.", 0.25);
par { show(bBox, 0.3); show(bCirc, 0.3); }
wait(0.3);
par { fade(bBox, 0.2); fade(bCirc, 0.2); }
par { show(onlyBox, 0.4); show(onlyCirc, 0.4); }
flash(onlyBox, teal); flash(onlyCirc, coral);
show(labBool, 0.25);
wait(0.4);
show(bHit, 0.45); flash(bHit, gold);
wait(0.7);
par {
fade(whole, 0.35);
fade(onlyBox, 0.35); fade(onlyCirc, 0.35); fade(bHit, 0.35); fade(labBool, 0.35);
cam((cx, cy), 0.6, smooth); zoom(1.0, 0.6, smooth);
}
}
}
step("contact") {
seq {
say(head, "contact points", 0.3);
say(cap, "a solid dot hides the crossing. a dashed ring keeps it.", 0.3);
par {
show(crossH, 0.05); show(crossV, 0.05);
draw(crossH, 0.5); draw(crossV, 0.5);
cam((cx, cy), 0.7, smooth); zoom(1.55, 0.7, smooth);
}
show(badDot, 0.3);
flash(badDot, coral);
wait(0.55);
fade(badDot, 0.25);
show(goodRing, 0.05); draw(goodRing, 0.55);
pulse(goodRing);
wait(0.7);
par {
fade(crossH, 0.35); fade(crossV, 0.35); fade(goodRing, 0.35);
cam((cx, cy), 0.7, smooth); zoom(1.0, 0.7, smooth);
}
}
}
step("settle") {
seq {
say(head, "fills for subjects. outlines for truth.", 0.35);
say(cap, "shapecraft: the visual is true — never hide what you're proving.", 0.35);
show(hero, 0.2);
become(hero, finalForm, 1.0, smooth);
pulse(hero);
wait(1.6);
}
}
Text & UI
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
text-guide
How text works, demonstrated. The one primitive every scene uses, and the one with the most
behaviour behind it: what a bare label does, what happens to a line too long for where it sits
(it wraps to the room it has — it is never clipped), how align pins an edge, what leading
does to line height, the two different ways to run text downwards (rot turns the line and takes
the letters with it; vertical stacks them upright), why caption exists beside text at all
— one entity per word, so karaoke can time them — and how hue colours a label, or a caption
one word at a time. Each section shows the call beside the result, so the guide is its own worked
example.
// How text works — a guide you can watch
//
// Text is the one primitive every scene uses, and the one with the most behaviour hiding behind it.
// This is that behaviour, demonstrated: what a bare `text` does, what happens to a line too long for
// where it sits, how to pin an edge, how to set line height, the two different ways to run text
// downwards, and why `caption` exists beside `text` at all.
//
// Each section shows the call and the result side by side, so the guide is also its own worked
// example — read the source next to the frame it produces.
title("how text works");
canvas("16:9");
template("black");
text(brand, (640, 26), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);
text(head, (640, 74), "How text works");
size(head, 34);
bold(head);
color(head, fg);
// ── 1 · the default ──
text(s1, (640, 150), "1 · a bare label is one line, centred on its point");
size(s1, 20);
color(s1, gold);
hidden(s1);
text(c1, (640, 200), "text(hello, (640, 300), \"one line\", 30)");
size(c1, 19);
color(c1, dim);
hidden(c1);
text(hello, (640, 300), "one line");
size(hello, 30);
color(hello, fg);
hidden(hello);
dot(anchor1, (640, 300), 4);
color(anchor1, coral);
hidden(anchor1);
// ── 2 · too long for its room ──
text(s2, (640, 150), "2 · a line too long for its room wraps — it is never clipped");
size(s2, 20);
color(s2, gold);
hidden(s2);
text(c2, (640, 200), "no wrap set: the renderer uses the room the point actually has");
size(c2, 19);
color(c2, dim);
hidden(c2);
text(longone, (400, 320), "This label sits left of centre, so it can only use twice its distance to the near edge — and that is exactly what it wraps to.");
size(longone, 21);
color(longone, cyan);
hidden(longone);
rect(room, (400, 320), 760, 130);
outlined(room);
outline(room, dim);
stroke(room, 1);
hidden(room);
text(c2b, (640, 470), "wrap(id, 300) chooses a narrower column than the frame allows");
size(c2b, 19);
color(c2b, dim);
hidden(c2b);
text(narrow, (980, 340), "A column you chose yourself, three hundred pixels wide.");
size(narrow, 19);
color(narrow, magenta);
wrap(narrow, 300);
hidden(narrow);
// ── 3 · which edge is pinned ──
text(s3, (640, 150), "3 · align pins an edge to the point");
size(s3, 20);
color(s3, gold);
hidden(s3);
line(rule3, (640, 230), (640, 560));
color(rule3, dim);
stroke(rule3, 1);
untraced(rule3);
text(al, (640, 270), "align(id, left)\nboth lines start here");
size(al, 21);
color(al, cyan);
align(al, left);
hidden(al);
text(ac, (640, 390), "align(id, center)\nthe default");
size(ac, 21);
color(ac, fg);
hidden(ac);
text(ar, (640, 510), "align(id, right)\nboth lines end here");
size(ar, 21);
color(ar, coral);
align(ar, right);
hidden(ar);
// ── 4 · line height ──
text(s4, (640, 150), "4 · leading is line height, as a multiple of the size");
size(s4, 20);
color(s4, gold);
hidden(s4);
text(l1, (300, 380), "leading\n1.0\ntight");
size(l1, 22);
color(l1, cyan);
leading(l1, 1.0);
hidden(l1);
text(l2, (640, 380), "default\n1.4\nprose");
size(l2, 22);
color(l2, fg);
hidden(l2);
text(l3, (980, 380), "leading\n2.2\nairy");
size(l3, 22);
color(l3, magenta);
leading(l3, 2.2);
hidden(l3);
// ── 5 · downwards, two different ways ──
text(s5, (640, 150), "5 · downwards: rotate the line, or stack the letters");
size(s5, 20);
color(s5, gold);
hidden(s5);
text(c5a, (300, 250), "rot(id, -90)");
size(c5a, 19);
color(c5a, dim);
hidden(c5a);
text(rotated, (300, 420), "ROTATED", 34);
color(rotated, cyan);
rot(rotated, -90);
hidden(rotated);
text(c5b, (640, 250), "rot(id, 90)");
size(c5b, 19);
color(c5b, dim);
hidden(c5b);
text(rotated2, (640, 420), "ROTATED", 34);
color(rotated2, teal);
rot(rotated2, 90);
hidden(rotated2);
text(c5c, (980, 250), "vertical(id)");
size(c5c, 19);
color(c5c, dim);
hidden(c5c);
text(stacked, (980, 420), "UPRIGHT", 34);
color(stacked, gold);
vertical(stacked);
hidden(stacked);
text(s5b, (640, 610), "the letters turn with the line, or stay the right way up — that is the whole difference");
size(s5b, 17);
color(s5b, fg);
hidden(s5b);
// ── 6 · caption is text per word ──
text(s6, (640, 150), "6 · caption makes one entity PER WORD, so words can be timed");
size(s6, 20);
color(s6, gold);
hidden(s6);
text(c6, (640, 210), "text(id, …) is one entity · caption(id, …) is {id}.w0, {id}.w1, …");
size(c6, 19);
color(c6, dim);
hidden(c6);
text(oneunit, (640, 320), "one entity, moves and fades as a whole");
size(oneunit, 22);
color(oneunit, cyan);
hidden(oneunit);
caption(perword, "one entity per word so each can be timed", (640, 440), 22, gold);
hidden(perword);
text(s6b, (640, 560), "which is what karaoke and wordpop need — and why a caption cannot wrap");
size(s6b, 17);
color(s6b, fg);
hidden(s6b);
// ── 7 · colour by hue, and per word ──
text(s7, (640, 150), "7 · hue colours text by angle — and a caption word by word");
size(s7, 20);
color(s7, gold);
hidden(s7);
text(c7, (640, 210), "hue(id, degrees, [saturation], [lightness]) · to(id, hue, 320, 2) cycles it");
size(c7, 19);
color(c7, dim);
hidden(c7);
text(h1, (330, 300), "hue(h1, 200)", 26);
hue(h1, 200);
hidden(h1);
text(h2, (950, 300), "hue(h2, 40, 0.9, 0.55)", 26);
hue(h2, 40, 0.9, 0.55);
hidden(h2);
caption(spectrum, "one hue per word across a caption", (640, 420), 26, fg);
for i in 0..6 { hue(spectrum.w{i}, i*54, 0.95, 0.62); }
hidden(spectrum);
text(s7b, (640, 530), "a caption is words, so each word can take its own colour");
size(s7b, 17);
color(s7b, fg);
hidden(s7b);
text(s7c, (640, 585), "gradients are shapes-and-strokes only — text is tinted, not filled");
size(s7c, 18);
color(s7c, dim);
hidden(s7c);
// ═══════════════════════════════════════════════════════════════════
// the run
// ═══════════════════════════════════════════════════════════════════
wait(0.6);
// 1 · the default
par { show(s1, 0.5); show(c1, 0.4); }
par { show(hello, 0.5); show(anchor1, 0.3); }
wait(2.4);
// 2 · overflow
par { fade(s1, 0.4); fade(c1, 0.4); fade(hello, 0.4); fade(anchor1, 0.3); }
par { show(s2, 0.5); show(c2, 0.4); }
par { show(room, 0.5); show(longone, 0.6); }
wait(2.6);
par { show(c2b, 0.4); show(narrow, 0.6); }
wait(2.6);
// 3 · alignment
par { fade(s2, 0.4); fade(c2, 0.4); fade(c2b, 0.4); fade(room, 0.4); fade(longone, 0.4); fade(narrow, 0.4); }
show(s3, 0.5);
draw(rule3, 0.6);
show(al, 0.5);
show(ac, 0.5);
show(ar, 0.5);
wait(2.6);
// 4 · leading
par { fade(s3, 0.4); fade(rule3, 0.4); fade(al, 0.4); fade(ac, 0.4); fade(ar, 0.4); }
show(s4, 0.5);
par { show(l1, 0.5); show(l2, 0.5); show(l3, 0.5); }
wait(2.6);
// 5 · downwards
par { fade(s4, 0.4); fade(l1, 0.4); fade(l2, 0.4); fade(l3, 0.4); }
show(s5, 0.5);
par { show(c5a, 0.4); show(rotated, 0.5); }
par { show(c5b, 0.4); show(rotated2, 0.5); }
par { show(c5c, 0.4); show(stacked, 0.5); }
wait(0.6);
show(s5b, 0.5);
wait(2.6);
// 6 · caption
par {
fade(s5, 0.4); fade(c5a, 0.4); fade(c5b, 0.4); fade(c5c, 0.4);
fade(rotated, 0.4); fade(rotated2, 0.4); fade(stacked, 0.4); fade(s5b, 0.4);
}
par { show(s6, 0.5); show(c6, 0.4); }
par { show(oneunit, 0.5); pulse(oneunit); }
wait(0.8);
show(perword, 0.4);
karaoke(perword, 3.2);
wait(0.4);
show(s6b, 0.5);
wait(2.4);
// 7 · hue
par { fade(s6, 0.4); fade(c6, 0.4); fade(oneunit, 0.4); fade(perword, 0.4); fade(s6b, 0.4); }
par { show(s7, 0.5); show(c7, 0.4); }
par { show(h1, 0.5); show(h2, 0.5); }
wait(0.5);
show(spectrum, 0.5);
wait(0.6);
par { show(s7b, 0.4); show(s7c, 0.4); }
to(h1, hue, 320, 2.4);
wait(2.6);
typewriter
Text revealed character by character.
// Typewriter — text revealed letter by letter (Manim's AddTextLetterByLetter),
// then removed letter by letter (RemoveTextLetterByLetter). `type` animates the
// text's `trace` 0->1 (a fraction of characters shown); `erase` runs it back.
// Declare the text `untraced` so it starts hidden, then `type` reveals it.
//
// manic examples/typewriter.manic
title("Typewriter");
canvas("16:9");
text(head, (cx, 150), "text, one letter at a time");
display(head); color(head, cyan); size(head, 26); hidden(head);
// untraced = trace 0 = no characters shown yet
text(line1, (cx, 320), "the quick brown fox"); color(line1, lime); size(line1, 44); untraced(line1);
text(line2, (cx, 400), "jumps over the lazy dog"); color(line2, cyan); size(line2, 44); untraced(line2);
cursor(line2); // this line types with a trailing cursor
// --- script ---
show(head, 0.5);
type(line1, 1.6); // AddTextLetterByLetter
type(line2, 1.6); // AddTextLetterByLetter, with a cursor
wait(1.0);
section("...and back");
erase(line2, 1.0); // RemoveTextLetterByLetter
erase(line1, 1.0);
wait(0.8);
captions
Karaoke / word-pop caption modes.
// Captions — karaoke word highlighting and TikTok-style word pop-in. `caption`
// lays out a phrase's words in a centred row (as {id}.w0, {id}.w1, ... tagged
// {id}.words); `karaoke` highlights them in sequence; `wordpop` pops them in one
// at a time.
//
// manic examples/captions.manic
// manic examples/captions.manic --record out --fps 60
title("Captions");
canvas("16:9");
text(head, (cx, 110), "word-by-word: karaoke + pop-in");
display(head); color(head, cyan); size(head, 26); hidden(head);
// karaoke: starts dim, words light up in sequence
caption(kara, "follow the bouncing highlight", (cx, 280), 46, dim);
// word-pop: hidden first, then each word pops in
caption(pop, "each word pops right in", (cx, 440), 50, lime);
hidden(pop.words);
// --- script ---
show(head, 0.5);
section("Karaoke");
karaoke(kara, 0.34, cyan);
wait(0.8);
section("Word pop");
wordpop(pop, 0.14);
wait(1.6);
terminal_boot
The neon terminal template booting up.
// Terminal Boot — a fake boot sequence typed out line by line, ending at a live
// prompt with a blinking-style cursor. Shows off the `cursor` modifier, `type`
// typewriter reveal, an author-set `masthead`, and the `terminal` template.
//
// manic examples/terminal_boot.manic
// manic examples/terminal_boot.manic --record out --fps 60
title("manic");
canvas("16:9");
template("terminal");
masthead("manic ~ %", "READY"); // your own header text (no engine branding)
text(l1, (cx, 210), ""); color(l1, lime); size(l1, 24);
text(l2, (cx, 260), ""); color(l2, cyan); size(l2, 24); hidden(l2);
text(l3, (cx, 310), ""); color(l3, cyan); size(l3, 24); hidden(l3);
text(l4, (cx, 360), ""); color(l4, lime); size(l4, 24); hidden(l4);
text(prompt, (cx, 440), ""); color(prompt, fg); size(prompt, 28); display(prompt);
hidden(prompt); cursor(prompt); // only the live prompt gets the cursor
// --- boot log ---
say(l1, "> initializing manic engine", 0.1);
type(l1, 1.0);
show(l2, 0.2);
say(l2, " loaded kits: std math geo algo brand", 0.1);
type(l2, 1.3);
show(l3, 0.2);
say(l3, " timeline: deterministic @ 60fps", 0.1);
type(l3, 1.0);
show(l4, 0.2);
say(l4, " ready.", 0.1);
type(l4, 0.5);
// --- the prompt, awaiting input ---
show(prompt, 0.2);
say(prompt, "manic ~ % render my_idea", 0.1);
type(prompt, 1.2);
wait(1.6);
brace
The curly-brace family.
// Braces — label spans and parts with curly braces, manic's Brace / BraceLabel
// / BraceBetweenPoints. A length is split into two parts a and b; a brace under
// each names it, and a brace over the whole names the sum. Every brace here is
// a BraceBetweenPoints (two points + a depth); bracelabel adds the text.
//
// manic examples/brace.manic
// manic examples/brace.manic --record out --fps 60
title("Braces");
canvas(1280, 720);
text(head, (640, 120), "label a span, or its parts");
display(head); color(head, cyan); size(head, 28); hidden(head);
text(cap, (640, 620), ""); color(cap, dim); size(cap, 24);
// the length, split at x = 680
line(seg, (300, 360), (980, 360)); color(seg, fg); stroke(seg, 3); untraced(seg);
dot(dl, (300, 360)); dot(dm, (680, 360)); dot(dr, (980, 360));
color(dl, magenta); color(dm, lime); color(dr, cyan);
hidden(dl); hidden(dm); hidden(dr);
// braces under the two parts, and over the whole (bulges up: points go R->L)
bracelabel(ba, (300, 392), (680, 392), "a", 30); color(ba, magenta);
bracelabel(bb, (680, 392), (980, 392), "b", 30); color(bb, cyan);
bracelabel(bt, (980, 320), (300, 320), "a + b", 34); color(bt, lime);
hidden(ba); hidden(bb); hidden(bt);
// --- reveal ---
show(head, 0.5);
say(cap, "here is a length");
draw(seg, 0.7);
par { show(dl, 0.3); show(dr, 0.3); }
wait(0.3);
section("Two parts");
say(cap, "split it at a point into parts a and b");
show(dm, 0.3);
show(ba, 0.5);
show(bb, 0.5);
wait(0.5);
section("The whole");
say(cap, "the whole span is a + b");
show(bt, 0.6);
par { pulse(ba.label); pulse(bb.label); pulse(bt.label); }
wait(1.4);
banner
The manic logo / banner reveal.
// The manic banner & watermark (à la ManimBanner). "create" draws the icon
// trio on; "expand" reveals the wordmark; the watermark persists in the corner.
//
// manic examples/banner.manic
// manic examples/banner.manic --record out --fps 60
title("manic");
canvas(1280, 720);
banner(logo, (600, 360), 1.1);
untraced(logo.icon); // icon shapes drawn on
hidden(logo.word); // wordmark revealed on "expand"
// a persistent, screen-fixed watermark, bottom-right
watermark(wm, (1120, 690), "manic // synthwave");
text(cap, (640, 560), ""); color(cap, dim); size(cap, 22);
// --- create: trace the icon trio on (staggered) ---
say(cap, "create");
stagger(0.2) {
draw(logo.dot, 0.6);
draw(logo.sq, 0.6);
draw(logo.tri, 0.6);
}
par { pulse(logo.dot); pulse(logo.sq); pulse(logo.tri); }
wait(0.4);
// --- expand: reveal the wordmark ---
say(cap, "expand");
show(logo.word, 0.6);
wait(1.2);
// --- unwrite: fade the whole banner ---
say(cap, "");
par { fade(logo.icon, 0.5); fade(logo.word, 0.5); }
wait(0.8);
equation
Display-quality LaTeX for fractions, roots, sums, powers and integrals, with semantic colour and template tinting.
// LaTeX math typesetting via `equation(...)`. Put the LaTeX in BACKTICKS so the
// backslashes survive. Rendered white-on-transparent and tinted by the entity
// colour, so `color(...)` and `template(...)` both work.
title("Equations");
canvas("16:9");
equation(quad, (cx, 150), `x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`, 68);
color(quad, cyan);
equation(sum, (cx, 340), `\sum_{k=1}^{n} k = \frac{n(n+1)}{2}`, 60);
equation(euler, (cx, 500), `e^{i\pi} + 1 = 0`, 64);
color(euler, gold);
equation(intg, (cx, 640), `\int_0^1 x^2\,dx = \tfrac{1}{3}`, 56);
color(intg, lime);
inline-math
Inline LaTeX mixed with ordinary prose, including wrapped explanatory text and a standalone display equation.
// Inline LaTeX everywhere: wrap math in `$…$` (backtick raw string) inside ANY
// text/caption/kit label. Plain text (no `$`) is unchanged. Mixed lines wrap.
title("Inline math");
canvas("16:9");
text(t1, (cx, 110), `The area of a circle is $A = \pi r^2$`);
color(t1, cyan);
// mixed text + math that WRAPS across lines
text(t2, (cx, 300), `By the Pythagorean theorem, if $a^2 + b^2 = c^2$ then the triangle with sides a, b, c is right-angled`);
wrap(t2, 760);
color(t2, lime);
// a whole-formula label (auto-typeset, centred)
equation(eq, (cx, 470), `\int_0^{1} x^2 \, dx = \tfrac{1}{3}`, 60);
color(eq, gold);
image
Embed a raster image (PNG/JPG) with image(id, (x,y), "asset:name.png"|"path", w, h) —
a bundled or user-provisioned file animated like any entity (shown, spun, pulsed, moved).
Unlocks logos, avatars and photo backdrops (e.g. a creator’s brand in a template).
// ============================================================================
// image.manic — embed a raster image (PNG/JPG) in a scene
// ----------------------------------------------------------------------------
// `image(id, (x,y), "asset:name.png"|"path", [w], [h])` loads a bundled or
// user-provisioned image file and draws it
// centred at (x,y), w×h px — and it's an ordinary entity, so every verb
// (`show`/`move`/`fade`/`pulse`/`spin`/…) animates it. Here a bundled logo
// slides + fades in, pulses, then a caption types beside it. A missing ordinary
// path draws a crossed placeholder; a missing bundled asset is an error.
//
// (This unlocks real logos, avatars and photo backdrops for creator templates —
// a creator drops their brand image into a slot.)
// ============================================================================
title("Raster Images");
canvas("16:9");
image(logo, (cx, 340), "asset:manic-logo.png", 300, 300); hidden(logo);
text(cap, (cx, 600), "any PNG/JPG — and it animates like anything else");
color(cap, fg); size(cap, 34); bold(cap); untraced(cap);
// ================= THE SCENE =================
show(logo, 0.6); pulse(logo);
wait(0.4);
spin(logo, 1.0);
type(cap, 2.0);
wait(0.6);
par { move(logo, (cx, 320), 0.8, smooth); pulse(logo); }
wait(1.0);
Generative & recursive
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
spiral-families
The six spirals nature keeps reusing, side by side, with no narration at all — every panel is
one closed-form formula and thousands of points of light, and the plate explains itself.
FIBONACCI r = aphi^(2t/pi) (nautilus, galaxies), VOGEL 137.5 degrees per seed (sunflowers,
pinecones), ARCHIMEDEAN r = a + bt (watch springs), FERMAT r = asqrt(t) with BOTH arms
(lens design), LOGARITHMIC r = ae^(bt) with three arms (hurricane rainbands), and the real
CURLICUE - the running sum of unit steps each turned by piphi*m^2, which a cloud can never
do (its formulas are pure in (i,t) and cannot accumulate), so it is computed exactly by
build-time sum reductions over the loop index and drawn as 360 real segments. Physics
respected: the log spirals sample uniformly in RADIUS, since their arc length grows with
radius, and each panel unfurls from its centre because opacity is saturate((t-start)*k - i/N)
- arithmetic, not keyframes. The background is the same law as wallpaper: level sets of
(angle - ln r / b) ARE logarithmic spirals. Then the UZUMAKI finale: one bound
parameterdraws all six families off their panels into a single CHAOTIC maelstrom - every point riding its own pitch, arm and phase from fract(sin(i)) hashes, with noise kneading the radius - while a torn-spiralshadervortex reads the same number and rises with them.
// spiral-families — the six spirals nature keeps reusing, side by side, each one a single
// closed-form formula and about five thousand points of light.
//
// Fibonacci r = a·φ^(2θ/π) nautilus shells, galaxies
// Vogel θ = n · 137.5° sunflower seeds, pinecones
// Archimedean r = a + bθ watch springs, coiled rope
// Fermat r = a·√θ optical lenses (both arms)
// Logarithmic r = a·e^(bθ) hurricanes (three arms)
// Curlicue φ = 2πφ·n² fractal art
//
// Every panel is one `cloud`: position, size and colour are closed-form functions of the
// point index `i` and live time `t`, so each spiral genuinely turns yet the whole plate stays
// a pure function of `t` — it scrubs and records exactly. The unfurl is not a keyframe
// either: each point's opacity is `saturate((t − start)·rate − i/N)`, so the light travels
// out from the centre because of arithmetic, not animation.
//
// Two honest notes. A LOGARITHMIC spiral has arc length proportional to radius, so the
// Fibonacci and hurricane panels sample uniformly in RADIUS — that is what makes their
// windings even instead of piling up at the rim. And the curlicue here is the quadratic-angle
// form: a cloud formula is pure in `(i, t)`, so it cannot accumulate the running sum of unit
// steps the classical curlicue is built from.
//
// manic examples/spiral-families.manic
title("Six Spirals Nature Keeps Reusing — manic");
canvas("16:9");
template("black");
bloom(0.38, 0.46, 26);
// the mark, above everything, for the whole film
text(brand, (640, 28), "maniclang.com");
display(brand); size(brand, 19); color(brand, cyan); opacity(brand, 0.8); plate(brand, 0.5); z(brand, 100);
text(ttl, (640, 70), "Six spirals nature keeps reusing");
display(ttl); size(ttl, 30); bold(ttl); color(ttl, fg); hidden(ttl);
// A background that obeys the same law the panels do: the level sets of (angle − ln r / b)
// ARE logarithmic spirals, so this is one giant log spiral used as wallpaper. Its eye sits
// below the frame, so the plate gets broad sweeping arms instead of a bullseye behind the
// grid, and the very top stays clean where the mark and the title live. Kept in a 0.02–0.10
// brightness band on purpose: it has to elevate the six spirals, never compete with them.
shader(bg) {
let x = (u - 0.5)*asp*1.25;
let y = v + 0.62;
let rr = length(x, y) + 0.02;
let a = atan2(y, x);
let ph = a - log(rr)/0.42;
let arms = 0.5 + 0.5*sin(2.0*ph + t*0.16);
let fine = 0.5 + 0.5*sin(5.0*ph - t*0.09);
let swirl = 0.68*arms + 0.32*fine;
let grain = 0.5 + 0.5*fbm(x*3.4 + t*0.02, y*3.4);
let top = smoothstep(0.0, 0.3, v);
let hue = 238 - 34.0*swirl;
let sat = 0.76 - 0.22*swirl;
let val = 0.016 + 0.078*swirl*top + 0.013*grain*top;
}
z(bg, -10);
// UZUMAKI — how far the whole plate has been drawn into a single spiral. Every panel's cloud
// reads this parameter BY NAME, so the finale is not six separate animations: it is one number,
// and each swarm swirls toward the centre because its own formula says so.
parameter(pull, (150, 690), 0, 0, 1, "uzumaki", 2); hidden(pull.widget);
shader(vortex) {
let x = (u - 0.5)*asp;
let y = v - 0.5;
let rr = length(x, y) + 0.02;
let a = atan2(y, x);
// a violent domain warp: the ANGLE itself is kneaded by noise, so the arms tear as they turn
let w = 0.6*snoise(x*3.2 + t*0.15, y*3.2 - t*0.1);
let ph = a + w - log(rr)/0.17;
let arms = 0.5 + 0.5*sin(4.0*ph + t*1.1);
let core = gaussian(rr, 0.17);
let edge = saturate(1.25 - rr*1.15);
let hue = 292 - 46.0*arms + 34.0*core;
let sat = 0.86 - 0.34*core;
let val = (0.05 + 0.52*arms*arms + 0.55*core)*edge;
let alpha = pull*saturate(0.12 + 1.15*arms*arms + core)*edge;
}
z(vortex, -5);
// ============================== panel furniture ==============================
// three columns, two rows: names above each spiral, its formula under the name, and what
// grows that way underneath the light
text(n1, (235, 116), "Fibonacci"); text(n2, (640, 116), "Vogel");
text(n3, (1045, 116), "Archimedean"); text(n4, (235, 398), "Fermat");
text(n5, (640, 398), "Logarithmic"); text(n6, (1045, 398), "Curlicue");
display(n1); display(n2); display(n3); display(n4); display(n5); display(n6);
size(n1, 22); size(n2, 22); size(n3, 22); size(n4, 22); size(n5, 22); size(n6, 22);
bold(n1); bold(n2); bold(n3); bold(n4); bold(n5); bold(n6);
hue(n1, 45); hue(n2, 92); hue(n3, 190); hue(n4, 215); hue(n5, 320); hue(n6, 272);
hidden(n1); hidden(n2); hidden(n3); hidden(n4); hidden(n5); hidden(n6);
equation(f1, (235, 150), `r = a\,\varphi^{2\theta/\pi}`, 21);
equation(f2, (640, 150), `\theta_n = n \cdot 137.5^{\circ}`, 21);
equation(f3, (1045, 150), `r = a + b\,\theta`, 21);
equation(f4, (235, 432), `r = a\sqrt{\theta}`, 21);
equation(f5, (640, 432), `r = a\,e^{b\theta}`, 21);
equation(f6, (1045, 440), `z_n = \sum_{m<n} e^{i\pi\varphi m^2}`, 16);
hue(f1, 45); hue(f2, 92); hue(f3, 190); hue(f4, 215); hue(f5, 320); hue(f6, 272);
hidden(f1); hidden(f2); hidden(f3); hidden(f4); hidden(f5); hidden(f6);
text(w1, (235, 366), "nautilus shells · galaxies");
text(w2, (640, 366), "sunflower seeds · pinecones");
text(w3, (1045, 366), "watch springs · coiled rope");
text(w4, (235, 648), "optical lenses");
text(w5, (640, 648), "hurricanes");
text(w6, (1045, 648), "fractal art");
display(w1); display(w2); display(w3); display(w4); display(w5); display(w6);
size(w1, 17); size(w2, 17); size(w3, 17); size(w4, 17); size(w5, 17); size(w6, 17);
color(w1, dim); color(w2, dim); color(w3, dim);
color(w4, dim); color(w5, dim); color(w6, dim);
hidden(w1); hidden(w2); hidden(w3); hidden(w4); hidden(w5); hidden(w6);
// ============================== 1 · FIBONACCI ==============================
// the golden spiral: every quarter turn multiplies the radius by φ = 1.618…, which is a
// logarithmic spiral with b = ln(φ)/(π/2) = 0.3063. Sampled uniformly in RADIUS, because a
// log spiral's arc length grows with its radius.
cloud(s1, 5200, gold, 0.85) {
let u = i/5200;
let rr = 1.2 + 76*u;
let th = log(rr/0.04)/0.3063 + 0.16*t;
let px = 235 + rr*cos(th);
let py = 258 - rr*sin(th);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 0.9 + 1.5*u;
let hue = 38 + 26*u;
let sat = 0.85;
let val = 0.72 + 0.28*u;
let alpha = saturate((t - 1.0)*2.4 - u*1.9);
}
glow(s1, 2);
// ============================== 2 · VOGEL ==============================
// phyllotaxis: seed n at 137.5° from the last and √n out. No two seeds crowd, which is why
// sunflowers, pinecones and pineapples all settle on this one.
cloud(s2, 1500, lime, 0.9) {
let n = i + 1;
let u = i/1500;
let rr = 78*sqrt(n/1500);
let th = n*2.39996 + 0.16*t;
let px = 640 + rr*cos(th);
let py = 258 - rr*sin(th);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 1.3 + 1.4*u;
let hue = 76 + 40*u;
let sat = 0.8;
let val = 0.7 + 0.3*u;
let alpha = saturate((t - 2.0)*2.4 - u*1.9);
}
glow(s2, 2);
// ============================== 3 · ARCHIMEDEAN ==============================
// equal spacing every turn — the coil of a watch spring or a rope on a deck. Sampled
// uniformly in θ, since that IS the defining regularity.
cloud(s3, 5200, cyan, 0.85) {
let u = i/5200;
let th = u*37.7;
let rr = 3.5 + 1.98*th;
let px = 1045 + rr*cos(th + 0.16*t);
let py = 258 - rr*sin(th + 0.16*t);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 1.0 + 1.1*u;
let hue = 184 + 24*u;
let sat = 0.8;
let val = 0.72 + 0.28*u;
let alpha = saturate((t - 3.0)*2.4 - u*1.9);
}
glow(s3, 2);
// ============================== 4 · FERMAT ==============================
// r = a√θ, and the real thing has BOTH arms — `mod(i,2)` picks one, so the panel shows the
// full双 curve. Equal AREA per turn, which is why lens and mirror designers use it.
cloud(s4, 5200, cyan, 0.85) {
let u = i/5200;
let arm = mod(i, 2)*pi;
let th = u*30;
let rr = 14.2*sqrt(th);
let px = 235 + rr*cos(th + arm + 0.16*t);
let py = 540 - rr*sin(th + arm + 0.16*t);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 1.0 + 1.0*u;
let hue = 206 + 26*u;
let sat = 0.82;
let val = 0.7 + 0.3*u;
let alpha = saturate((t - 4.0)*2.4 - u*1.9);
}
glow(s4, 2);
// ============================== 5 · LOGARITHMIC ==============================
// the same law as Fibonacci with a fatter pitch, and three arms — a hurricane's rainbands.
// Again sampled uniformly in radius; the bright core is the eye.
cloud(s5, 5400, magenta, 0.85) {
let u = i/5400;
let arm = mod(i, 3)*2.0944;
let rr = 1.0 + 77*u;
let th = log(rr/1.6)/0.30 + arm + 0.34*t;
let px = 640 + rr*cos(th);
let py = 540 - rr*sin(th);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 0.9 + 1.4*u;
let hue = 300 + 40*u;
let sat = 0.78;
let val = 0.95 - 0.3*u;
let alpha = saturate((t - 5.0)*2.4 - u*1.9);
}
glow(s5, 2);
// ============================== 6 · CURLICUE ==============================
// The REAL curlicue, not a stand-in: z_n is the running sum of unit steps, each turned by
// π·s·m². A `cloud` cannot do this — its formulas are pure in (i, t) and cannot accumulate —
// but a build-time `sum` reduction over the loop index computes the exact partial sum, so the
// path is drawn as 360 real segments. The golden fraction makes the classic branching,
// self-similar clusters; nothing here is random and nothing is recursive.
for n in 0..360 {
line(s6{n},
(975 + 6.5*sum(m in 0..n : cos(pi*0.618034*m*m)),
566 - 6.5*sum(m in 0..n : sin(pi*0.618034*m*m))),
(975 + 6.5*sum(m in 0..n+1 : cos(pi*0.618034*m*m)),
566 - 6.5*sum(m in 0..n+1 : sin(pi*0.618034*m*m))));
hue(s6{n}, 258 + n/11);
untraced(s6{n});
tag(s6{n}, s6);
}
glow(s6, 2);
// ---- the uzumaki finale ----
svg(maki1, (250, 366), "asset:svg/emoji/1f365.svg", 74); hidden(maki1);
svg(maki2, (1030, 366), "asset:svg/emoji/1f365.svg", 74); hidden(maki2);
text(uzulab, (640, 648), "UZUMAKI");
display(uzulab); size(uzulab, 38); bold(uzulab); color(uzulab, fg); plate(uzulab, 0.62); z(uzulab, 50); hidden(uzulab);
// ================================= the film =================================
show(ttl, 1.0);
wait(0.5);
// each panel introduces itself as its own light arrives — the name, the formula and what grows
// that way are already on screen, so the film does not narrate them
stagger(1.0) {
par { show(n1, 0.5); show(f1, 0.5); show(w1, 0.4); }
par { show(n2, 0.5); show(f2, 0.5); show(w2, 0.4); }
par { show(n3, 0.5); show(f3, 0.5); show(w3, 0.4); }
par { show(n4, 0.5); show(f4, 0.5); show(w4, 0.4); }
par { show(n5, 0.5); show(f5, 0.5); show(w5, 0.4); }
par { show(n6, 0.5); show(f6, 0.5); show(w6, 0.4); }
}
draw(s6, 2.4, smooth);
wait(1.0);
// they all turn, so the dwell is not dead time
wait(4.0);
wait(3.6);
// ============================== UZUMAKI ==============================
par {
fade(n1, 0.7); fade(n2, 0.7); fade(n3, 0.7); fade(n4, 0.7); fade(n5, 0.7); fade(n6, 0.7);
fade(f1, 0.7); fade(f2, 0.7); fade(f3, 0.7); fade(f4, 0.7); fade(f5, 0.7); fade(f6, 0.7);
fade(w1, 0.6); fade(w2, 0.6); fade(w3, 0.6); fade(w4, 0.6); fade(w5, 0.6); fade(w6, 0.6);
fade(ttl, 0.8);
}
wait(1.4);
// one number does all of this: each swarm reads `pull` and swirls in on its own account,
// and the curlicue path swings round with them
par {
to(pull, value, 1, 4.6, smooth);
turn(s6, (640, 360), 80, 4.6, smooth);
to(s6, opacity, 0.2, 4.6, smooth);
}
wait(1.8);
// the merged spiral gets a beat on its own, then steps back so the word can sit on it
par {
to(s1, opacity, 0.17, 1.0); to(s2, opacity, 0.17, 1.0); to(s3, opacity, 0.17, 1.0);
to(s4, opacity, 0.17, 1.0); to(s5, opacity, 0.17, 1.0);
}
par { show(maki1, 0.7); show(maki2, 0.7); }
show(uzulab, 0.9);
wait(2.8);
// ================================= endcard =================================
par {
fade(maki1, 0.6); fade(maki2, 0.6);
fade(uzulab, 0.7);
to(pull, value, 0.42, 1.6, smooth);
}
wait(2.8);
art-golden-angle
The golden angle as glowing particle art: 1600 seeds bloom from the centre (angle = i·137.5°, radius = √i) into a sunflower. Nudge the divergence a fraction off φ and spiral voids tear open — only 137.5° packs the head seamlessly. Pure cloud, additive glow.
// The Golden Angle — why sunflowers spiral. 1600 seeds, each placed one turn of
// 137.5° from the last (φ's angle), at radius √i. That single angle packs the
// plane with no gaps and no seam — nudge it a fraction and spiral voids tear open.
// Pure formula-driven `cloud`: angle = i·div, radius = √i.
//
// manic examples/art-golden-angle.manic
title("The Golden Angle — 137.5°");
canvas("16:9");
template("black");
text(hdr, (640, 74), "The Golden Angle — Manic", 32);
cloud(seeds, 1600) {
let g = 137.507; // φ's angle: 360·(1 − 1/φ) degrees
// between t≈5 and t≈8 the divergence dips 0.7° off golden — watch gaps open
let bump = 0.25 * (1 + tanh((t - 5.0) * 2.4)) * (1 + tanh((8.0 - t) * 2.4));
let div = g - bump * 0.7;
let ang = i * div * 0.0174533; // degrees → radians
let rad = 7.3 * sqrt(i + 0.5); // √i spacing → uniform density
let x = 640 + rad * cos(ang);
let y = 392 + rad * sin(ang);
// bloom: seeds appear from the centre outward over the first ~3 s
let born = i / 1600;
let alpha = 0.5 * (1 + tanh((t - born * 3.0 - 0.4) * 4));
let hue = mod(48 - rad * 0.14, 360); // gold core → magenta rim
let sat = 0.9;
let val = 0.62; // <1 shows hue; glow re-brightens
let rnd = mod(sin(i * 17.1) * 43758.5453, 1);
let r = 2.9 + 1.4 * rnd; // round discs (>2.5px), size grain
}
// additive glow: overlapping seeds bloom into light — a lit sunflower head
glow(seeds, 4);
text(cap, (640, 700), "1600 seeds, each turned 137.5° from the last.", 22);
hidden(cap);
wait(0.8);
show(cap);
wait(2.6);
say(cap, "The golden angle — φ's turn. Perfect packing, no seam.");
wait(2.0);
say(cap, "A fraction off, and spiral voids tear open…");
wait(2.8);
say(cap, "…only 137.5° fills the head without a gap.");
wait(2.2);
art-circle-area-proof
Area = πr² as a SWARM: the same particles fill a disc, then flow into a parallelogram of the same area whose scalloped wedge-edges refine and flatten toward a rectangle (the limit). Nothing is added or removed — the conserved count IS the proof.
// Area = πr², as a SWARM — and the LIMIT that finishes the proof.
// The same particles fill a disc, flow into a lumpy wedge-strip, then the humps
// MULTIPLY and FLATTEN (4 → 8 → 16 wedges …) until the edge is straight: a
// πr × r rectangle. Nothing is added or removed — the count is the area (πr²),
// conserved the whole way. One `cloud`, all formula-driven.
//
// manic examples/art-circle-area-proof.manic
title("Area of a circle = πr²");
canvas("16:9");
template("black");
// on-screen heading, top-centre, held throughout
text(hdr, (640, 74), "Circle Area of Proof — Manic", 32);
cloud(swarm, 3200) {
// ---- uniform grid index → (fx, fy) in the unit square ---------------------
let cols = 80;
let ci = mod(i, cols);
let ri = (i - ci) / cols; // integer row 0..39
let fx = ci / 79; // 0..1 across the width
let fy = ri / 39; // 0..1 top → bottom
// a little hash jitter so the grid reads as a filled field, not a lattice
let jx = (mod(sin(i * 12.9898) * 43758.5453, 1) - 0.5) * 7;
let jy = (mod(sin(i * 78.2330) * 43758.5453, 1) - 0.5) * 7;
// ---- destination: a parallelogram with SCALLOPED (wedge) edges ------------
let wdt = 565; let hlf = 90; // base πr ≈ 565, height r = 180
let x0 = 313; let yc = 340; let slnt = 90;
// refinement s: 0 (few coarse wedges) → 1 (many fine wedges → rectangle)
let s = 0.5 * (1 + tanh((t - 5.6) * 0.7));
let nh = 2 + 6 * s; // humps per edge: 2 → 8
let amp = 48 * (1 - s) + 2; // hump depth: 50 → 2 (flattens)
let wv = amp * cos(6.2831853 * nh * fx);
let topE = yc - hlf - wv; // top edge bulges up at the humps
let botE = yc + hlf + wv; // bottom edge bulges down
let sx = x0 + fx * wdt + (1 - fy) * slnt + jx;
let sy = topE + fy * (botE - topE) + jy;
// ---- start: a uniform disc of the SAME area (golden-angle sunflower) ------
let gr = sqrt((i + 0.5) / 3200);
let ang = i * 2.399963;
let dx = 640 + 180 * gr * cos(ang);
let dy = 340 + 180 * gr * sin(ang);
// ---- blend disc → strip, then the strip refines to a rectangle ------------
let b = 0.5 * (1 + tanh((t - 3.2) * 1.1));
let x = dx * (1 - b) + sx * b;
let y = dy * (1 - b) + sy * b;
let hue = mod(330 - gr * 140, 360); // Manic neon: magenta core → cyan rim
let sat = 0.9;
let val = 0.6; // <1 shows the hue; additive glow re-brightens overlaps
// varied radius > 2.5px → true ROUND discs (≤2.5px render as squares), with size grain
let rnd = mod(sin(i * 91.7) * 43758.5453, 1);
let r = 2.8 + 2.2 * rnd;
}
// additive glow: dense/overlapping points accumulate into light — soft nebula cores
glow(swarm, 4);
text(cap, (640, 630), "π r² particles — a disc's worth.", 24);
hidden(cap);
wait(0.6);
show(cap);
wait(1.8);
say(cap, "Cut into wedges and re-lay them — a lumpy strip.");
wait(2.2);
say(cap, "More wedges, finer and finer — the humps flatten…");
wait(2.6);
say(cap, "…in the limit, a πr × r rectangle. Area = π r².");
wait(2.4);
art-calculus-sine
Sine, its derivative, its Riemann area and a riding tangent — five index-partitioned particle families in one cloud, cross-faded in beats. Thick glowing wave-ribbons with bright cores over a particle coordinate-grid: the swarm is the subject, not a plotted line.
// Sine, its derivative, its Riemann area & a riding tangent — all PARTICLE ART.
// 12000 dots in five families, one formula, no plot/coords/riemann built-ins:
// 0 SINE ribbon (cyan) — a thick glowing wave-swarm
// 1 COSINE ribbon (gold) — the derivative, cos x = the slope
// 2 coordinate GRID (faint) — the x/y plane in dots
// 3 RIEMANN columns (magenta)— particles fill the strips under the wave
// 4 riding TANGENT swarm (white) — a line that tilts to cos x as it sweeps
// The families CROSS-FADE in beats so each idea reads on its own, then a finale.
//
// manic examples/art-calculus-sine.manic
title("Sine · derivative · area");
canvas("16:9");
template("black");
text(hdr, (640, 70), "Sine · its Derivative · its Area — Manic", 30);
cloud(field, 12000) {
let g = floor(i / 2400); // family 0..4
let m0 = clamp(1 - max(g, -g), 0, 1);
let m1 = clamp(1 - max(g-1, 1-g), 0, 1);
let m2 = clamp(1 - max(g-2, 2-g), 0, 1);
let m3 = clamp(1 - max(g-3, 3-g), 0, 1);
let m4 = clamp(1 - max(g-4, 4-g), 0, 1);
let li = mod(i, 2400);
let loc = li / 2399;
let ox = 640; let oy = 384;
let sx = 92; let sy = 118;
let mx = (loc * 2 - 1) * 3.14159; // math x ∈ [-π, π]
let ph = mx + t * 0.9; // the wave travels (gentle)
let sp = rand(i) + rand(i + 4051) - 1; // -1..1, dense near 0
let asp = max(sp, -sp);
// 0/1 — thick sine & cosine ribbons
let wvX = ox + mx * sx;
let sinY = oy - (sin(ph) + sp * 0.24) * sy;
let cosY = oy - (cos(ph) + sp * 0.24) * sy;
// 2 — faint particle grid
let gridX = ox + (mod(li, 52) / 51 * 2 - 1) * 320;
let gridY = oy - (floor(li / 52) / 51 * 2 - 1) * 178;
// 3 — Riemann columns: 16 strips, particles fill axis → sin height (signed)
let bi = floor(loc * 16);
let barMX = 0.0 - 3.14159 + (bi + 0.5) / 16 * 6.28318;
let barH = sin(barMX + t * 0.9);
let rmX = ox + barMX * sx + (rand(i + 11) - 0.5) * (6.28318 / 16 * sx * 0.78);
let rmY = oy - rand(i + 23) * barH * sy;
// 4 — a tangent line that sweeps and tilts to the slope cos(x0)
let x0 = 0.0 - 2.3 + mod(t * 0.4, 1) * 4.6;
let ss = (loc * 2 - 1) * 0.9;
let tanH = sin(x0 + t * 0.9) + cos(x0 + t * 0.9) * ss; // value + slope·offset
let tgX = ox + (x0 + ss) * sx;
let tgY = oy - tanH * sy + (rand(i + 77) - 0.5) * 8;
let x = (m0 + m1) * wvX + m2 * gridX + m3 * rmX + m4 * tgX;
let y = m0 * sinY + m1 * cosY + m2 * gridY + m3 * rmY + m4 * tgY;
// ---- beats: each idea rises, then clears for the next ----------------------
let rmA = clamp((t - 4.5) * 1.0, 0, 1) * clamp((10.0 - t) * 1.0, 0, 1); // area 4.5–10
let tgA = clamp((t - 9.5) * 1.0, 0, 1); // tangent 9.5→end
let cosDim = 1 - 0.55 * rmA; // derivative steps back while area shows
let hue = m0 * 192 + m1 * 46 + m2 * 210 + m3 * 328 + m4 * 50;
let sat = m0 * 1.0 + m1 * 1.0 + m2 * 0.3 + m3 * 0.8 + m4 * 0.3;
let core = 0.5 * (1 - 0.7 * asp);
let val = m0 * core + m1 * core + m2 * 0.14 + m3 * 0.32 + m4 * 0.75;
let alpha = m0 * 1.0 + m1 * cosDim + m2 * 0.45 + m3 * rmA * 0.8 + m4 * tgA;
let r = m2 * 1.8 + (m0 + m1) * (2.4 + rand(i + 88))
+ m3 * (2.2 + rand(i + 5)) + m4 * (2.7 + rand(i + 9));
}
glow(field, 2);
text(cap, (640, 700), "cyan sin x · gold cos x — its slope.", 22);
hidden(cap);
wait(1.0);
show(cap);
wait(3.4);
say(cap, "Riemann strips — the area under the wave, in dots.");
wait(5.0);
say(cap, "The sum clears; a tangent rides — its tilt IS cos x.");
wait(4.0);
say(cap, "A function, its slope, its area — one swarm.");
wait(3.2);
exponential-shells
A living de Sitter volume — ∭ a(t) ∝ e^{Ht} rendered as ~320,000 points of light. A grainy
multicolour spherical CAP meets a family of exponentially-growing spherical SHELLS tangent at a
shared hot origin — one cloud3 batch per colour family, every point a pure closed-form f(i). A
dense gold junction glows additively (glow) into the white-hot core; bloom() gives the milky
cosmic light. A full 25s camera3 orbit turns the face-on concentric rings into the offset 3-D
shells and loops cleanly back. Showcases cloud3 at scale + per-point sat/val, additive glow,
and the bloom post-process together.
// engine-test-13 — native 3D reconstruction of the reference.
//
// The picture is not a flat disc. It is a large, grainy spherical particle cap
// meeting a family of exponentially growing particle shells at a shared hot
// origin. Looking down their common axis makes concentric rings; a full camera
// orbit reveals the offset spherical shells and returns to the opening frame.
title("∭ 𝘢(𝘵) ∝ eᴴᵗ");
canvas(1638, 1482);
template("black");
bloom(0.90, 0.22, 52);
// Keep the mathematical title fixed in screen space while the 3-D field turns.
text(formulaTitle, (819, 70), "∭ 𝘢(𝘵) ∝ eᴴᵗ");
size(formulaTitle, 44);
color(formulaTitle, gold);
bold(formulaTitle);
display(formulaTitle);
sticky(formulaTitle);
z(formulaTitle, 20);
// The camera begins on the cap side of the common tangent. It keeps turning in
// one direction throughout the 20-second hold, completing 1.5 revolutions.
// That puts opposite face-on views about 6.67 s apart, matching the reference
// cadence; four fast turns made the alternating side views read as oscillation.
// The wider field of view keeps the luminous rim inside the complete orbit.
camera3((-32, 0, 0), (0, 0, 0), 21.0, perspective);
// --- large spherical cap --------------------------------------------------
// Several low-opacity random skins give the reference its fine, multicolour
// grain. Surface-point foreshortening naturally creates the bright rim.
cloud3(outerRose, 52000, #d78676, 0.095) {
let ct = -1 + 1.18 * rand2(i, 10.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 11.9);
let rr = 4.66 + 0.075 * (rand2(i, 11.3) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.42 + 0.58 * (-ct);
let r = 0.042;
}
glow(outerRose, 1);
cloud3(outerViolet, 48000, #72589f, 0.072) {
let ct = -1 + 1.18 * rand2(i, 22.7);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 24.3);
let rr = 4.69 + 0.09 * (rand2(i, 23.9) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.30 + 0.70 * (1 + ct);
let r = 0.038;
}
glow(outerViolet, 1);
cloud3(outerSilver, 36000, #b8d8ef, 0.072) {
let ct = -1 + 1.18 * rand2(i, 36.3);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 38.7);
let rr = 4.72 + 0.055 * (rand2(i, 37.1) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.22 + 0.78 * (1 + ct);
let r = 0.034;
}
glow(outerSilver, 1);
// A sparse warm skin just outside the main boundary produces the thin amber
// fringe visible around the lavender rim in the reference.
cloud3(outerAmber, 18000, #d67425, 0.026) {
let ct = -1 + 1.18 * rand2(i, 50.3);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 52.9);
let rr = 4.79 + 0.08 * (rand2(i, 51.7) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.35 + 0.65 * (-ct);
let r = 0.036;
}
glow(outerAmber, 1);
// A broad, extremely faint splat layer closes the gaps between the fine
// grains. Additive accumulation turns it into the milky cosmic illumination
// visible in the recording without replacing the surface texture.
cloud3(outerCosmos, 90000, #b99bbd, 0.012) {
let ct = -1 + 1.18 * rand2(i, 118.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 121.7);
let rr = 4.69 + 0.10 * (rand2(i, 119.9) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.32 + 0.68 * (-ct);
let r = 0.085;
}
glow(outerCosmos, 1);
// --- exponential shell family -------------------------------------------
// Every sphere is tangent at the origin. The gold family grows inward with
// centre=(-radius,0,0); pink/violet/cyan grow outward from (+radius,0,0).
// Exponential radius growth turns the face-on rings into the nested horn seen
// edge-on. `s` selects one sphere and `j` selects a deterministic surface point;
// each colour family remains one efficient renderer batch.
cloud3(shellGold, 33600, #ffad24, 0.080) {
let per = 4200;
let s = floor(i / per);
let j = i - s * per;
let sr = 0.080 * exp(0.310 * s);
let ct = 1 - 2 * rand2(j, s + 63.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(j, s + 65.7);
let rr = sr + 0.012 * (rand2(i, s + 4.2) - 0.5);
let x = -sr + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.018 + 0.0012 * s;
}
glow(shellGold, 1);
cloud3(shellPink, 9200, #ff79c6, 0.075) {
let per = 4600;
let s = floor(i / per);
let j = i - s * per;
let k = s + 8;
let sr = 0.105 * exp(0.218 * k);
let ct = 1 - 2 * rand2(j, k + 73.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(j, k + 75.7);
let rr = sr + 0.012 * (rand2(i, k + 4.2) - 0.5);
let x = sr + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.028;
}
glow(shellPink, 1);
cloud3(shellViolet, 10000, #c398ff, 0.065) {
let per = 5000;
let s = floor(i / per);
let j = i - s * per;
let k = s + 10;
let sr = 0.105 * exp(0.218 * k);
let ct = 1 - 2 * rand2(j, k + 83.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(j, k + 85.7);
let rr = sr + 0.012 * (rand2(i, k + 4.2) - 0.5);
let x = sr + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.030;
}
glow(shellViolet, 1);
cloud3(shellCyan, 16800, #b9ffff, 0.070) {
let per = 5600;
let s = floor(i / per);
let j = i - s * per;
let k = s + 12;
let sr = 0.105 * exp(0.218 * k);
let ct = 1 - 2 * rand2(j, k + 93.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(j, k + 95.7);
let rr = sr + 0.012 * (rand2(i, k + 4.2) - 0.5);
let x = sr + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.032;
}
glow(shellCyan, 1);
// Dense gold dust at the shared tangent becomes the white-hot crescent when
// viewed from the side and the tiny luminous bullseye when viewed end-on.
cloud3(junction, 7600, #ffd45a, 0.14) {
let ct = 1 - 2 * rand2(i, 103.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 105.7);
let rr = 0.095 * (0.45 + 0.55 * rand2(i, 71.2));
let x = 0.02 + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.026;
}
glow(junction, 1);
orbit3(720, 0, 32, 20, linear);
shader-glitch-grid
A p5 WEBGL multi-pass sketch — a randomly generated grid pattern, RGB-shifted into a glitch —
reimagined as ONE per-pixel shader. The original pre-renders four grid/stripe layers into
off-screen buffers, composites them, captures the result, then a second shader tears it; manic
glsl() can’t sample render targets, but the OUTCOME is closed-form: build the nested random
grid procedurally per pixel (floor/fract/rand2), then chromatically tear it by sampling
each colour channel at a per-scanline horizontal offset. Pure in (u,v,t) — the glitch scrubs
and records exactly where the p5 sketch only draws once.
// shader-glitch-grid — a p5 WEBGL multi-pass sketch ("Glitch animation of a randomly
// generated grid pattern") reimagined in ONE manic `shader`. The original pre-renders
// FOUR grid/stripe layers into off-screen buffers, composites them with a substitution
// shader (each coarse cell shows a different sub-pattern), captures the result, then a
// second shader RGB-shifts it into a glitch. manic `glsl()` can't take render-target
// textures — but the OUTCOME is closed-form: build the nested grid PROCEDURALLY per
// pixel, then chromatically tear it by sampling each colour channel at a per-scanline
// horizontal offset. Pure in (u,v,t): the glitch scrubs and records exactly.
//
// manic examples/shader-glitch-grid.manic
title("Glitch grid — a multi-pass shader, reimagined per-pixel");
canvas("1:1");
template("black");
shader(glitch) {
// per-scanline-block horizontal offset, re-randomised a few times a second, and
// faded IN after the grid has settled (the original delays the glitch too)
let band = floor(v * 40.0);
let gt = floor(t * 3.0);
let gon = smoothstep(3.5, 4.5, t);
let off = (rand2(band, gt) - 0.5) * 0.06 * gon;
// RED — the nested random grid sampled at u + off
let ru = u + off;
let rcx = floor(ru * 10.0); let rcy = floor(v * 10.0); let rh = rand2(rcx, rcy);
let rdot = step(0.2, fract(ru * 100.0)) * step(fract(ru * 100.0), 0.8)
* step(0.2, fract(v * 100.0)) * step(fract(v * 100.0), 0.8);
let rstr = step(0.5, fract(v * 50.0));
let cr = mix(0.08, mix(mix(0.90, 0.12, rdot), mix(0.93, 0.18, rstr), step(0.7, rh)), step(0.4, rh));
// GREEN — same grid at u + off*0.3 (slight chromatic split)
let gu = u + off * 0.3;
let gcx = floor(gu * 10.0); let gh = rand2(gcx, rcy);
let gdot = step(0.2, fract(gu * 100.0)) * step(fract(gu * 100.0), 0.8)
* step(0.2, fract(v * 100.0)) * step(fract(v * 100.0), 0.8);
let cg = mix(0.08, mix(mix(0.90, 0.12, gdot), mix(0.93, 0.18, rstr), step(0.7, gh)), step(0.4, gh));
// BLUE — same grid at u + off*1.2 (the widest split)
let bu = u + off * 1.2;
let bcx = floor(bu * 10.0); let bh = rand2(bcx, rcy);
let bdot = step(0.2, fract(bu * 100.0)) * step(fract(bu * 100.0), 0.8)
* step(0.2, fract(v * 100.0)) * step(fract(v * 100.0), 0.8);
let cb = mix(0.08, mix(mix(0.90, 0.12, bdot), mix(0.93, 0.18, rstr), step(0.7, bh)), step(0.4, bh));
// white noise on top (as the original adds), stronger while glitching
let n = (rand2(u * 700.0 + gt, v * 700.0) - 0.5) * (0.05 + 0.12 * gon);
let r = cr + n;
let g = cg + n;
let b = cb + n;
}
caption(head, "Glitch grid — one formula per pixel", (400, 44), 22);
hidden(head);
show(head);
wait(9);
shader-plasma
A fragment-shader-style colour field — the per-PIXEL twin of cloud. Every pixel’s colour is ONE
closed-form formula of its normalized coords u/v, time t and aspect asp (shader(bg){ let r/g/b = … }), re-evaluated each frame yet pure in t so it scrubs and records exactly. manic’s take
on The Book of Shaders: layered travelling sines make plasma, a smoothstep vignette frames it — no
per-pixel loops, no assets, just algebra. The GLSL shaping idioms (mix/smoothstep/clamp/fract/
length) are now shared by every formula-driven builtin.
// shader-plasma — a fragment-shader-style colour field, the per-PIXEL twin of
// `cloud`. Every pixel's colour is ONE closed-form formula of its normalized
// coordinates `u`/`v`, live time `t`, and aspect `asp` — re-evaluated each frame
// yet pure in `t`, so it scrubs and records exactly. This is manic's take on
// "The Book of Shaders" (thebookofshaders.com): no per-pixel loops, just algebra.
//
// manic examples/shader-plasma.manic
title("A shader — one formula, every pixel");
canvas("9:16");
template("black");
shader(bg) {
// centre + aspect-correct so the field is round, not stretched (u/v are 0..1
// on BOTH axes, so a raw circle would be an ellipse on a 9:16 canvas).
let x = (u - 0.5) * asp;
let y = v - 0.5;
let d = length(x, y);
// layered travelling sines = classic plasma
let p = sin(x*7.0 + t) + sin(y*7.0 + t*1.3) + sin((x + y)*5.0 - t*0.9) + sin(d*11.0 - t*1.6);
// vignette: bright centre → dark edges (so the title/caption read in white)
let vig = smoothstep(0.95, 0.2, d);
let r = (0.5 + 0.5*sin(p + t)) * vig;
let g = (0.5 + 0.5*sin(p + t + 2.1)) * vig;
let b = (0.5 + 0.5*sin(p + t + 4.2)) * vig;
}
// ---- textbook annotations ----
caption(head, "A shader — one formula per pixel", (540, 150), 32);
caption(sub, "no loops, no assets — just algebra of (u, v, t)", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `\text{colour} = f(u,\, v,\, t)`, 40);
caption(lab, "per-pixel, re-evaluated every frame — yet seekable", (540, 1800), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
show(lab);
wait(24);
shader-fractal
A LIVING Julia set in a shader field. Each pixel iterates z→z²+c and colours by escape speed —
the Book of Shaders ‘Fractals’ chapter, with NO per-pixel loop in the DSL: julia(zx,zy,cx,cy) runs
the iteration in the engine and returns an escape fraction. Sweeping the constant c in a circle over
t morphs the fractal through the whole Julia family, every frame still a pure function of time. The
escape-hatch that also gives mandelbrot(x,y) and voronoi(x,y) (cellular noise) without loops.
// shader-fractal — a LIVING Julia set. Each pixel iterates z = z² + c a fixed
// number of times and colours by how fast it escapes — the Book-of-Shaders
// "Fractals" chapter, but with NO per-pixel loop in the DSL: `julia(zx,zy,cx,cy)`
// runs the iteration in the engine and returns an escape fraction in [0,1]. We
// sweep the constant `c` in a circle over time, so the fractal morphs through the
// whole Julia family — every frame still a pure function of `t` (scrub-safe).
//
// manic examples/shader-fractal.manic
title("A living Julia set — one formula per pixel");
canvas("9:16");
template("black");
shader(bg) {
// complex plane, aspect-corrected and centred
let zx = (u - 0.5) * 3.0 * asp;
let zy = (v - 0.5) * 3.0;
// the constant c orbits slowly → the set continuously morphs
let cx = 0.7 * cos(t * 0.35);
let cy = 0.7 * sin(t * 0.35);
let e = julia(zx, zy, cx, cy); // escape fraction: 1 = trapped, 0 = flees
let inside = step(0.985, e); // 1 for the fractal body
let band = 0.5 + 0.5 * sin(e * 26.0 - t * 2.0); // rainbow escape contours
let glow = 1.0 - inside; // dark body, lit exterior
let r = band * glow;
let g = (0.4 + 0.6 * band) * glow;
let b = (1.0 - 0.5 * band) * glow + inside * 0.06;
}
// ---- textbook annotations ----
caption(head, "A living Julia set", (540, 150), 34);
caption(sub, "z → z² + c, coloured by escape speed", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `z_{n+1} = z_n^2 + c`, 44);
caption(lab, "no per-pixel loop in the DSL — the engine iterates", (540, 1800), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
show(lab);
wait(24);
lost-in-patterns
A learning-forward fractal odyssey in four movements, each a different KIND of infinity with its
generating RULE shown on screen: ∞ by ZOOM — a ×500 Mandelbrot dive (z→z²+c); ∞ by PARAMETER
— the SAME rule with c the knob, wiring the Mandelbrot→Julia bridge (each c in the set grows one
Julia); ∞ by ITERATION — a Clifford strange attractor from one cloud … from map rule fed its own
output 6,000×; ∞ by RECURSION — a Koch snowflake (lsystem) closing on the paradox: perimeter
3(4/3)ⁿ→∞ yet area→8/5·A₀. Four generators (shader · shader · cloud · lsystem) as one lesson, pure in t.
// lost-in-patterns — a fractal odyssey in four movements, each a different KIND
// of infinity, each with the RULE that generates it shown on screen (manic's
// thesis: the diagram is true, so the maths is visible):
// I ∞ by ZOOM the Mandelbrot set — z→z²+c, a x500 dive into seahorse valley
// II ∞ by PARAMETER the Julia family — SAME rule, but now c is the knob
// (the Mandelbrot set is the MAP of which c give a connected Julia — the
// bridge between movements I and II)
// III ∞ by ITERATION a Clifford strange attractor — no shape-formula, one rule fed
// its own output 6,000× (cloud … from map)
// IV ∞ by RECURSION the Koch snowflake — F→F+F--F+F, closing on the paradox:
// infinite perimeter, finite area.
title("Lost in Infinite Patterns");
canvas("16:9");
template("black");
// ---------- HUD ----------
text(head, (cx, 60), "Lost in Infinite Patterns"); display(head); cursor(head); sticky(head);
text(cap, (cx, h - 42), ""); size(cap, 26); sticky(cap);
counter(mag, (w - 170, 120), 1, 0, "zoom x", ""); color(mag, gold); hidden(mag);
// a dark card so the teaching panel stays legible over the bright fractals
polygon(panel, (230, 92), (768, 92), (768, 250), (230, 250), #05070d);
opacity(panel, 0.5); sticky(panel); hidden(panel);
// the "kind of infinity" chip — one per movement (show/hide, no narration)
text(kind1, (499, 122), "∞ by ZOOM"); size(kind1, 30); color(kind1, cyan); sticky(kind1); hidden(kind1);
text(kind2, (499, 122), "∞ by PARAMETER"); size(kind2, 30); color(kind2, magenta); sticky(kind2); hidden(kind2);
text(kind3, (499, 122), "∞ by ITERATION"); size(kind3, 30); color(kind3, gold); sticky(kind3); hidden(kind3);
text(kind4, (499, 122), "∞ by RECURSION"); size(kind4, 30); color(kind4, lime); sticky(kind4); hidden(kind4);
// the generating rule of each movement — the maths made visible
equation(eqIter, (499, 190), `z_{n+1} = z_n^{2} + c`, 34); sticky(eqIter); hidden(eqIter);
equation(eqC, (499, 236), `c = 0.7885\,e^{i\theta}`, 26); sticky(eqC); hidden(eqC);
equation(eqCliff,(499, 196), `\begin{cases} x' = \sin(ay)+c\cos(ax) \\ y' = \sin(bx)+d\cos(by) \end{cases}`, 24); sticky(eqCliff); hidden(eqCliff);
equation(eqKoch, (499, 188), `F \to F\,{+}\,F\,{-}{-}\,F\,{+}\,F`, 30); sticky(eqKoch); hidden(eqKoch);
// the closing paradox — the one genuine gasp
equation(eqPar, (cx, h - 120), `\text{perimeter } 3\left(\tfrac{4}{3}\right)^{n}\!\to\infty \qquad \text{area}\to \tfrac{8}{5}A_0`, 34); sticky(eqPar); hidden(eqPar);
// hidden journey axes (the sliders stay backstage — the dive/angle drive the shaders)
parameter(dive, (w - 180, 660), 0, 0, 1, "dive", 2); hidden(dive);
parameter(ang, (w - 180, 690), 0, 0, 6.283, "angle", 2); hidden(ang);
// ---------- I. the Mandelbrot coastline ----------
shader(mset) {
let sc = 3.2 * exp(-6.2 * dive);
let x = -0.743644 + (u - 0.5) * asp * sc;
let y = 0.131826 + (v - 0.5) * sc;
let m = mandelbrot(x, y);
let band = m^0.35;
let hue = 205 + 140 * band + 25 * sin(0.3 * t);
let sat = 0.75;
let val = (1 - step(0.999, m)) * (0.12 + 0.88 * band);
}
// ---------- II. the Julia bloom ----------
shader(jul) {
let x = (u - 0.5) * asp * 2.9;
let y = (v - 0.5) * 2.9;
let cr = 0.7885 * cos(ang);
let ci = 0.7885 * sin(ang);
let j = julia(x, y, cr, ci);
let band = j^0.4;
let hue = 290 + 120 * band + 15 * sin(0.4 * t);
let sat = 0.8;
let val = (1 - step(0.999, j)) * (0.1 + 0.9 * band);
}
hidden(jul);
// ---------- III. the strange attractor (Clifford, a=-1.4 b=1.6 c=1 d=0.7) ----
cloud(att, 6000, #ffffff, 0.85) from map("sin(-1.4*y)+cos(-1.4*x)", "sin(1.6*x)+0.7*cos(1.6*y)", (0.1, 0.1)) {
let wsp = 0.12 * t;
let px = hx * cos(wsp) - hy * sin(wsp);
let py = hx * sin(wsp) + hy * cos(wsp);
let x = 640 + 225 * px; // cloud formulas can't see cx/cy — hardcode the 16:9 centre
let y = 360 + 15 + 165 * py;
let r = 1.1 + 0.8 * mod(abs(sin(i * 12.9898) * 43758.55), 1);
let hue = 150 + 60 * hypot(hx, hy) + 25 * sin(0.4 * t + 0.002 * i);
}
hidden(att);
// ---------- IV. the snowflake (Koch, one closed stroke) ----------
lsystem(koch, (cx, cy + 20), 560, "F--F--F", "F=F+F--F+F", "angle=60 iterations=4 closed=true");
untraced(koch); stroke(koch, 3); gradient(koch, cyan, magenta, gold);
// the four fullscreen visuals sit BEHIND the HUD (all default to z=0, so without
// this the later-declared shader would paint over the teaching panel + captions)
z(mset, -3); z(jul, -3); z(att, -3); z(koch, -2);
// ================= timeline =================
// ---- I: ∞ by ZOOM — the Mandelbrot dive
type(head, 1.2);
par { show(panel, 0.4); show(kind1, 0.4); show(mag, 0.3); }
show(eqIter, 0.6);
par {
to(dive, value, 1, 12, smooth);
to(mag, value, 500, 12, smooth);
seq {
say(cap, "the Mandelbrot set: keep the points where z → z² + c never flies to infinity", 0.6);
wait(4.6);
say(cap, "zoom 500x and the coastline keeps unfolding — new seahorses, never one exact repeat", 0.5);
wait(4.2);
}
}
cue(whoosh);
// ---- bridge: the Mandelbrot set IS the map of Julia sets
say(cap, "here is the secret: every point c in that black island grows its OWN fractal...", 0.5);
wait(3.2);
// ---- II: ∞ by PARAMETER — same rule, c is the knob
par { fade(mset, 1.2); fade(mag, 0.6); fade(kind1, 0.3); show(jul, 1.2); show(kind2, 0.4); }
show(eqC, 0.5); // eqIter STAYS on screen — same rule, bridging I and II
par {
to(ang, value, 6.283, 12, linear);
seq {
say(cap, "...a Julia set. Freeze z's rule, make c the knob: c inside the set → connected, outside → dust", 0.5);
wait(5.0);
say(cap, "slide c around a circle and every value is a different universe — same equation, new world", 0.5);
wait(4.6);
}
}
cue(whoosh);
// ---- III: ∞ by ITERATION — a rule fed its own output
par { fade(jul, 1.4); fade(kind2, 0.3); fade(eqIter, 0.3); fade(eqC, 0.3); show(att, 1.6); show(kind3, 0.4); }
show(eqCliff, 0.5);
say(cap, "no formula draws this shape — just this rule, fed its own output six thousand times", 0.5);
wait(4.0);
say(cap, "nudge one constant and it's a whole new creature: that sensitivity IS chaos", 0.5);
wait(3.4);
cue(whoosh);
// ---- IV: ∞ by RECURSION — the snowflake, and the paradox
par { fade(att, 1.2); fade(kind3, 0.3); fade(eqCliff, 0.3); show(kind4, 0.4); }
show(eqKoch, 0.5);
say(cap, "some infinities you draw with one stroke: replace every edge with four, forever", 0.5);
draw(koch, 6.0);
par { cam((cx + 215, cy - 100), 1.6, smooth); zoom(2.8, 1.6, smooth); }
say(cap, "look closer — the edge is made of smaller edges, at every scale", 0.5);
wait(2.6);
par { cam((cx, cy), 1.5, smooth); zoom(1, 1.5, smooth); }
// the payoff: infinite perimeter, finite area
show(eqPar, 0.8);
say(cap, "the perimeter grows x4/3 every step — to INFINITY — yet the whole shape fits in a circle", 0.6);
wait(3.4);
cue(chime);
say(cap, "four rules, four infinities — and you are never done looking", 0.7);
wait(3.2);
shader-shapes
The Book of Shaders ‘Shapes’ chapter in a shader field: draw with DISTANCE and ANGLE. length(x,y)
is the radius, atan2(y,x) the angle; modulating the radius by the angle turns a circle into a
breathing 5-point star, and smoothstep cuts a crisp anti-aliased edge from the distance field —
pure polar algebra of (u,v,t), no paths.
// shader-shapes — the Book-of-Shaders "Shapes" chapter: draw with DISTANCE and
// ANGLE. `length(x,y)` is the distance to the centre, `atan2(y,x)` the angle;
// modulating the radius by the angle turns a circle into a star or flower, and
// `smoothstep` cuts a crisp (anti-aliased) edge from the distance field. All
// per-pixel, no paths — just polar algebra of (u, v, t).
//
// manic examples/shader-shapes.manic
title("Shapes from distance & angle — a shader");
canvas("9:16");
template("black");
shader(bg) {
let x = (u - 0.5) * asp; // aspect-correct so it's round, not oval
let y = v - 0.5;
let r = length(x, y); // distance to centre
let a = atan2(y, x); // angle (polar)
// a 5-point star: the edge radius breathes with the angle (and pulses in t)
let edge = 0.30 + 0.08*cos(a*5.0 + t) + 0.02*sin(t*2.0);
let body = smoothstep(edge + 0.006, edge - 0.006, r); // 1 inside the star
let hue = mod(a*57.3 + t*30.0, 360.0); // angle → rainbow rim
let sat = 0.9;
let val = 0.12 + 0.6*body; // dark field, lit star
}
caption(head, "Shapes from distance & angle", (540, 150), 32);
caption(sub, "length() = radius, atan2() = angle — polar SDF", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `r=\text{length}(x,y),\quad \theta=\operatorname{atan2}(y,x)`, 30);
hidden(eq);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
wait(22);
shader-cellular
The ‘Cellular noise’ (Worley/Voronoi) chapter: voronoi(x,y) scatters one feature point per unit
cell and returns the distance to the nearest, so the field organises into flowing cells. The 3x3
neighbour search runs in the engine — the DSL formula stays closed-form and seekable, no loop.
// shader-cellular — the Book-of-Shaders "Cellular noise" (Worley/Voronoi)
// chapter. `voronoi(x,y)` scatters one feature point per unit cell and returns
// the distance to the NEAREST one — so the field organises into cells: bright
// near a point, dark at the equidistant borders. Drifting the coordinate in `t`
// makes the cells flow. The 3×3 neighbour search runs inside the engine, so the
// formula stays a closed-form, seekable function of (u, v, t) — no loop in the DSL.
//
// manic examples/shader-cellular.manic
title("Cellular noise — a Voronoi shader");
canvas("9:16");
template("black");
shader(bg) {
let s = 5.5;
let d = voronoi(u*s*asp, v*s + t*0.4); // distance to nearest cell point
let edge = smoothstep(0.0, 0.08, d); // ~0 at borders → dark cracks
let tb = smoothstep(0.0, 0.14, v) * smoothstep(1.0, 0.86, v); // darken top/bottom so captions read
let hue = mod(d*200.0 + t*24.0, 360.0); // colour by distance
let sat = 0.8;
let val = (0.15 + 0.7*edge) * tb;
}
caption(head, "Cellular noise", (540, 150), 34);
caption(sub, "distance to the nearest of many scattered points", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `F_1(p)=\min_i\;\lVert p - q_i\rVert`, 34);
hidden(eq);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
wait(22);
shader-patterns
The ‘Patterns’ chapter: fract+floor turn ONE tile into an infinite grid. fract(u*n) is the
position inside each tile, floor(u*n) is which tile — so one motif (a glowing disc + pulsing ring)
repeats everywhere and the tile index drives per-tile hue. No copies, no loop, just algebra.
// shader-patterns — the Book-of-Shaders "Patterns" chapter: `fract` and `floor`
// turn ONE cell into an infinite grid. `fract(u*n)` is the position INSIDE each
// tile (0..1), `floor(u*n)` is which tile you're in — so one motif drawn in tile
// space repeats everywhere, and the tile index can drive per-tile colour. Pure
// algebra of (u, v, t): no copies, no loop.
//
// manic examples/shader-patterns.manic
title("Patterns — one tile, endlessly repeated");
canvas("9:16");
template("black");
shader(bg) {
let n = 6.0;
let gx = fract(u*n*asp) - 0.5; // local coord inside each tile
let gy = fract(v*n) - 0.5;
let d = length(gx, gy);
let dot = smoothstep(0.36, 0.30, d); // a disc per tile
let ring = smoothstep(0.02, 0.0, abs(d - (0.24 + 0.06*sin(t)))); // pulsing ring
let id = floor(u*n*asp) + floor(v*n); // which tile → per-tile hue
let hue = mod(id*24.0 + t*36.0, 360.0);
let sat = 0.85;
let val = 0.12 + 0.6*dot + 0.5*ring;
}
caption(head, "Patterns from one tile", (540, 150), 34);
caption(sub, "fract() = position in tile, floor() = which tile", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `\text{tile}(u)=\operatorname{fract}(u\,n),\ \operatorname{floor}(u\,n)`, 28);
hidden(eq);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
wait(22);
shader-shapes-dsl
Patricio’s glsl-shapes Shadertoy rebuilt as a FAITHFUL DSL twin using the shape/SDF builtin library:
each shape is ONE scalar call — sdpolygon/sdhexagon/sdstar/sdrhombus/sdroundbox — smin-
unioned and banded by an oscillating iso-line. True distance fields (no vec gymnastics), so the
concentric bands are exact offsets, not the polar approximation.
// shader-shapes-dsl — Patricio's glsl-shapes Shadertoy, now a FAITHFUL DSL twin using
// the new shape/SDF builtins (Layer 1). No vec2 algebra, no branches, no glsl — each
// shape is one scalar builtin (`sdpolygon`/`sdcircle`/`sdtriangle`/`sdhexagon`/
// `sdstar`/`sdrhombus`/`sdroundbox`), unioned, and banded by Patricio's oscillating
// iso-line. Transpiles to GLSL (full-res) with the CPU field as the exact fallback —
// so the SAME scene renders identically on both, unlike the old polar-approximation.
//
// manic examples/shader-shapes-dsl.manic
title("Patricio's shapes — faithful, in the shader DSL");
canvas("16:9");
template("black");
shader(shapes) {
let x = u * asp; // match Patricio's st = uv*vec2(asp,1)
let y = v;
// eight SDF shapes, each a single builtin (centres = Patricio's layout)
let s0 = sdpolygon(x - 0.48, y - 0.48, 0.076, 5); // pentagon
let s1 = sdcircle(x - 0.75, y - 0.80, 0.076); // circle
let s2 = sdtriangle(x - 0.21, y - 0.79, 0.076); // triangle
let s3 = sdpolygon(x - 0.16, y - 0.26, 0.076, 8); // octagon
let s4 = sdhexagon(x - 0.20, y - 0.50, 0.060); // hexagon
let s5 = sdstar(x - 0.79, y - 0.51, 0.11, 5); // 5-point star
let s6 = sdrhombus(x - 0.63, y - 0.17, 0.15, 0.07); // rhombus (≈ the ellipse)
let s7 = sdroundbox(x - 0.48, y - 0.79, 0.13, 0.05, 0.03); // rounded rectangle
// union — smin with a tiny k ≈ hard min (DSL `min` is a reduction, not callable)
let ua = smin(smin(smin(s0, s1, 0.003), s2, 0.003), s3, 0.003);
let ub = smin(smin(smin(s4, s5, 0.003), s6, 0.003), s7, 0.003);
let d = smin(ua, ub, 0.003);
// Patricio's oscillating iso-band + inside / thin-line masks
let band = floor(mod((d * 57.6 + t * 2.6) / 2.0, 1.0) * 2.0);
let outside = step(0.0, d);
let linem = step(0.0, d) * step(d, 0.006);
// two-tone palette: orange outside, blue inside (each banded); white iso-line
let br = mix(mix(0.431, 0.270, band), 1.000, outside);
let bgc = mix(mix(0.436, 0.190, band), mix(0.684, 0.514, band), outside);
let bbc = mix(1.000, mix(0.364, 0.128, band), outside);
let r = mix(br, 1.0, linem);
let g = mix(bgc, 1.0, linem);
let b = mix(bbc, 1.0, linem);
}
wait(8);
shader-fx
The shader/formula HELPER kit in action: fill(d,size,edge) and stroke(d,size,w,edge) turn an SDF
distance into a solid + a crisp outline, and gain shapes the gradient behind them. Clean vector-
style rendering from scalar builtins — SDF shapes + mask helpers, no glsl().
// shader-fx — the Layer-1 shader/formula helpers (fx kit) in action: `fill` and
// `stroke` turn an SDF distance into a solid + an outline, `gain` shapes a gradient.
// All scalar builtins, transpiled to GLSL with the CPU field as the exact fallback.
//
// manic examples/shader-fx.manic
title("fill · stroke · gain — SDF mask helpers");
canvas("16:9");
template("black");
shader(fx) {
let x = u * asp;
let y = v;
// three shapes, unioned
let s0 = sdhexagon(x - 0.60, y - 0.5, 0.15);
let s1 = sdstar(x - 1.05, y - 0.5, 0.17, 5);
let s2 = sdcircle(x - 1.48, y - 0.5, 0.13);
let d = smin(smin(s0, s1, 0.01), s2, 0.01);
// masks from the distance
let fl = fill(d, 0.0, 0.004); // 1 inside → 0 outside
let ol = stroke(d, 0.0, 0.03, 0.004); // bright band on the iso-line
// gain-shaped vertical gradient behind the shapes
let bgv = gain(v, 2.2);
let base = 0.10 + 0.20 * bgv;
// compose: gradient bg, teal fill, white outline
let r = mix(mix(base, 0.16, fl), 1.0, ol);
let g = mix(mix(base, 0.52, fl), 1.0, ol);
let b = mix(mix(base * 1.7, 0.62, fl), 1.0, ol);
}
wait(4);
shader-parameter
A shader driven by a scene parameter, not just by time: the ring frequency is a SLIDER the field
references by name, so the SAME shader re-renders as you sweep it. On the GPU it’s a u_freq uniform;
on the deterministic CPU fallback the live value is substituted into the formula — both in lock-step.
// shader-parameter — a `shader` driven by a scene `parameter`, not just by time.
// The ring frequency `freq` is a slider/parameter: the shader references it by
// name, so the SAME field re-renders as `freq` animates. On the GLSL path it's a
// `u_freq` uniform (resolved from the parameter each frame); on the CPU fallback
// the live value is substituted into the formula — both stay in lock-step.
//
// manic examples/shader-parameter.manic
title("A shader driven by a parameter");
canvas("16:9");
template("black");
parameter(freq, (640, 660), 3, 1, 14, "freq", 0);
shader(rings) {
let d = hypot((u - 0.5) * asp, v - 0.5); // distance from centre (aspect-correct)
let hue = mod(d * freq * 90.0, 360.0); // ring hue cycles faster as freq rises
let sat = 0.8;
let val = 0.55 + 0.35 * sin(d * freq * 18.0);
}
caption(head, "shader ← parameter", (640, 66), 34);
hidden(head);
show(head);
// sweep the parameter: the rings tighten as freq climbs 3 → 14
to(freq, value, 14, 6, smooth);
shader-warp
DOMAIN WARPING — the class of shader Layer-1 builtins can’t express, because you can’t rotate a scalar
coordinate. With vec2 + rot2 + swizzle you rotate SPACE itself (more the further from centre):
let p = vec2(...); let q = rot2(p, ang); … q.x … q.y. Real vector maths in the DSL — vectors are
erased to scalar Nodes at compile, so the swirl runs on both backends from one source.
// shader-warp — Layer 2: vec math in the DSL. The headline that Layer-1 builtins
// can't express — DOMAIN WARPING. You can't rotate a scalar coordinate; with vec2 +
// rot2 + swizzle you rotate SPACE itself (more the further from centre), then read a
// ring pattern in the warped frame. Pure DSL — no glsl(), no vec2 gymnastics beyond
// the builtins. vecs are erased to scalar math at compile, so it renders on both
// backends (CPU field + GLSL) from one source.
//
// manic examples/shader-warp.manic
title("Domain warp — vec2 + rot2 in the shader DSL");
canvas("16:9");
template("black");
shader(warp) {
let p = vec2(u * asp - 0.9, v - 0.5); // name the centred coordinate as a vec2
let ang = length(p) * 7.0 - t; // twist grows with radius
let q = rot2(p, ang); // rotate SPACE once; reuse the vec2
let hue = mod(200.0 + q.x * 500.0 + q.y * 300.0, 360.0);
let sat = 0.8;
let val = 0.5 + 0.4 * sin(q.y * 40.0);
}
wait(8);
raymarch-metaballs
Shader V2: a 3-D scene RAY-MARCHED per pixel. You write only the signed-distance field let d
(distance from any point x,y,z to the scene); the engine marches a ray per pixel to the surface,
takes the normal by finite differences and shades it — the per-pixel loop runs in the engine (like
voronoi/mandelbrot), and there are NO vec/mat types (the SDF is a scalar formula, component math
the manic way). Three spheres orbit and MERGE through smin (smooth union) into living metaballs.
// raymarch-metaballs — Shader V2: a 3-D scene RAY-MARCHED per pixel. You write
// only the signed-distance field `let d` (the distance from any point x,y,z to
// the scene); the engine marches a ray per pixel until it hits the surface,
// takes the normal by finite differences, and shades it. No per-pixel loop in
// the DSL (it runs in the engine, like `voronoi`/`mandelbrot`) and NO vec/mat
// types — the SDF is a scalar formula, component math the manic way. Here three
// spheres orbit and MERGE through `smin` (smooth union) into living metaballs.
//
// manic examples/raymarch-metaballs.manic
title("Metaballs — a ray-marched 3D field");
canvas("16:9");
template("black");
raymarch(blobs) {
// three moving spheres (signed distance = distance to centre − radius)
let a = sdsphere(x - 0.75*sin(t), y - 0.5*cos(t*1.3), z + 0.3*sin(t*0.7), 0.52);
let b = sdsphere(x + 0.6*cos(t*0.9), y + 0.45*sin(t*1.1), z - 0.35*cos(t), 0.46);
let c = sdsphere(x + 0.2*sin(t*1.7), y + 0.6*sin(t*0.7), z + 0.25*sin(t*1.4), 0.4);
// smooth-union them (smin) so they gloop together instead of just overlapping
let ab = smin(a, b, 0.55);
let d = smin(ab, c, 0.55);
}
// ---- textbook annotations ----
caption(head, "Metaballs — one distance field", (640, 66), 34);
caption(sub, "raymarch: you write the SDF, the engine marches it", (640, 122), 22);
hidden(head);
hidden(sub);
equation(eq, (640, 648), `d = \operatorname{smin}(d_1, d_2, k)`, 34);
hidden(eq);
show(head);
wait(1.6);
show(sub);
wait(2.6);
show(eq);
wait(22);
raymarch-sculpture
Shader V2.2: a coloured, carved SDF sculpture that a real camera3 orbits. New over the
metaballs: your own HIT colour (let hue/sat/val or r/g/b, a formula of the surface normal
nx/ny/nz, hit height hz and time t); camera3 reuse so orbit3 sweeps the scene; and the
SDF boolean toolkit — smin (smooth union), sdsub (carve a shape out), sdint (intersect). Still
one scalar distance field — no per-pixel loop, no vec types.
// raymarch-sculpture — Shader V2.2: a coloured, carved SDF sculpture that a real
// `camera3` orbits. New since V2.1: (1) your own HIT colour — `let hue`/`sat`/`val`
// (or r/g/b) as a formula of the surface normal `nx`/`ny`/`nz`, hit height `hz`
// and time `t`; (2) `camera3` reuse — the marcher builds its rays from the scene
// camera, so `orbit3` sweeps around the scene; (3) SDF booleans `smin` (smooth
// union), `sdsub` (carve), `sdint` (intersect). Still just a scalar distance
// field — no per-pixel loop in the DSL, no vec types.
//
// manic examples/raymarch-sculpture.manic
title("A carved, coloured SDF — orbited by camera3");
canvas("16:9");
template("black");
camera3((3.4, -3.8, 2.2), (0, 0, 0.15), 38, perspective);
raymarch(gem) {
// a core sphere with two bumps smoothly fused on (metaball style)
let core = sdsphere(x, y, z, 1.0);
let b1 = sdsphere(x - 0.9*sin(t*0.8), y, z + 0.9*cos(t*0.8), 0.44);
let b2 = sdsphere(x + 0.5*cos(t), y - 0.85*sin(t*1.1), z, 0.4);
let blob = smin(smin(core, b1, 0.45), b2, 0.45);
// carve a spherical bite out of it
let bite = sdsphere(x - 0.55, y - 0.9, z + 0.55, 0.62);
let d = sdsub(bite, blob);
// iridescent colour: hue from the facing direction + a slow time sweep
let hue = mod(205.0 + nx*95.0 + ny*55.0 + t*40.0, 360.0);
let sat = 0.82;
let val = 0.52 + 0.28*nz;
}
caption(head, "A carved, coloured SDF", (640, 66), 34);
caption(sub, "hit colour from the normal + camera3 orbit", (640, 122), 22);
hidden(head);
hidden(sub);
show(head);
wait(1.4);
show(sub);
// slow camera orbit — the marcher re-reads camera3 every frame (captions stay up)
orbit3(70, 0, 5.4, 24, smooth);
raymarch-boxgrid
Shader V2.3, the finale: the raymarched Shadertoy that started the thread (tssSDN — a grid of
boxes rippling around a bouncing sphere), rebuilt as a TRUE ray-march (not the cloud3 reimagining).
One SDF: rep(x,r) tiles a box into an infinite grid (sdbox3), each cell’s HEIGHT a wave of its
rand2 hash + a falloff from the moving sphere, smin-unioned with the sphere. Coloured red→gold by
height, orbited by camera3 — the per-pixel march runs in the engine, no vec types, one formula.
// raymarch-boxgrid — Shader V2.3, the finale: the raymarched Shadertoy that
// started this whole thread (tssSDN — a grid of boxes rippling around a bouncing
// sphere), rebuilt as a TRUE ray-march this time (V2 tier), not the cloud3
// reimagining (examples/cloud3-ripple.manic). One signed-distance field does it:
// `rep(x,r)` tiles a box into an infinite grid (`sdbox3`), each cell's HEIGHT a
// wave of its `rand2` hash + a falloff from the moving sphere's position; `smin`
// unions in the sphere. Per-pixel loop in the ENGINE, no vec types — just a
// scalar distance formula. Coloured red→gold by height, orbited by `camera3`.
//
// manic examples/raymarch-boxgrid.manic
title("A ray-marched box grid — the Shadertoy, rebuilt");
canvas("16:9");
template("black");
camera3((2.6, -3.2, 2.2), (0, 0, 0.25), 40, perspective);
raymarch(grid) {
let rp = 0.42; // cell size
let idx = floor(x / rp); // which cell (x)
let idy = floor(y / rp); // which cell (y)
let lx = rep(x, rp); // local coord inside the cell
let ly = rep(y, rp);
let sx = sin(t * 1.8) * 1.3; // the bouncing sphere (inlined)
let sy = cos(t * 2.2) * 1.3;
let cxx = idx * rp + rp * 0.5; // this cell's centre
let cyy = idy * rp + rp * 0.5;
let bs = hypot(cxx - sx, cyy - sy); // cell → sphere distance
let fall = 1 - smoothstep(0.0, 2.2, bs); // near the sphere ⇒ taller
let hsh = rand2(idx, idy); // per-cell phase
let bh = 0.34 + 0.30 * sin(hsh * 6.283 + t * 2.6 + bs * 1.7) * fall; // box height (>0)
let box = sdbox3(lx, ly, z - bh * 0.5, rp * 0.4, rp * 0.4, bh * 0.5);
let ball = sdsphere(x - sx, y - sy, z - 0.6, 0.18);
let d = smin(box, ball, 0.04); // grid ∪ sphere
// colour: red troughs → gold crests, top faces brighter
let hue = mod(6.0 + bh * 42.0, 360.0);
let sat = 0.85;
let val = 0.28 + 0.55 * nz;
}
caption(head, "A ray-marched box grid", (640, 66), 34);
caption(sub, "sdbox3 + rep() tiling + smin — the Shadertoy, in manic", (640, 122), 22);
hidden(head);
hidden(sub);
show(head);
wait(1.5);
show(sub);
orbit3(50, 0, 4.6, 22, smooth);
raymarch-docker-latency
Data as geometry: Docker daemon socket latency as a raymarched LIQUID MESH. The daemon sits at
the origin emitting high-frequency concentric pings (amplitude ∝ a jittery round-trip-time signal);
three containers fire expanding ring events at baked timestamps, each ring’s reach ∝ its measured
RTT. Every cell of a sdbox3+rep() grid samples that field at its centre, so the mesh shimmers
with socket traffic — one scalar SDF, marched by the engine, coloured by hit height. Honest by
design: manic is pure in t, so the trace is BAKED (not a live socket) — swap the constants for a
captured docker events log and it replays deterministically. Data → SDF displacement → raymarch.
// raymarch-docker-latency — "Visualizing Docker daemon socket latency as a raymarched
// fluid surface." A per-pixel ray-marched LIQUID MESH: a grid of columns whose heights
// ARE a latency trace. The daemon socket sits at the origin and emits high-frequency
// concentric pings (amplitude modulated by a jittery round-trip-time signal); three
// containers fire expanding ring events at baked timestamps, each ring's reach ∝ its
// measured RTT. Every cell samples that field at its centre → the mesh shimmers with
// socket traffic. One scalar SDF (`sdbox3` + `rep()` tiling), marched by the engine.
//
// Honest note: manic is PURE IN t (that's what lets it scrub + record), so it does NOT
// tail a live /var/run/docker.sock in real time. The trace is BAKED IN — timestamps and
// RTTs as constants — so the same second always renders the same wavefront. Swap the
// constants for a captured `docker events` / socket-latency log and the mesh replays it
// deterministically: data → SDF displacement → raymarch, exactly as described. The data
// source is a recording, not a socket; the mechanism is real.
//
// manic examples/raymarch-docker-latency.manic
title("Docker daemon socket latency — a raymarched liquid mesh");
canvas("16:9");
template("black");
camera3((2.4, -3.3, 2.0), (0, 0, 0.2), 40, perspective);
raymarch(fluid) {
let rp = 0.34; // mesh cell size
let idx = floor(x / rp); let idy = floor(y / rp);
let lx = rep(x, rp); let ly = rep(y, rp);
let cx = idx*rp + rp*0.5; let cy = idy*rp + rp*0.5; // this cell's centre
let r0 = hypot(cx, cy); // distance from the daemon socket (origin)
// baked latency signal: socket round-trip time, jittery + bursty
let lat = 0.5 + 0.28*sin(t*5.3) + 0.16*sin(t*11.7 + 1.3) + 0.10*sin(t*23.1 + 0.7);
// the daemon socket: high-frequency concentric pings, amplitude ∝ latency
let pings = lat * sin(6.0*r0 - t*7.0) / (1.0 + 1.3*r0);
// three containers talking to the daemon: baked (epicenter, fire time, RTT) rings
let d1 = hypot(cx + 1.3, cy - 0.8); let a1 = t - 1.4; let f1 = a1*1.9;
let e1 = step(0.0, a1) * exp(-0.7*a1) * sin(7.0*(d1 - f1)) * exp(-3.0*(d1-f1)*(d1-f1));
let d2 = hypot(cx - 1.6, cy - 1.1); let a2 = t - 3.2; let f2 = a2*2.1;
let e2 = step(0.0, a2) * exp(-0.6*a2) * sin(7.0*(d2 - f2)) * exp(-3.0*(d2-f2)*(d2-f2));
let d3 = hypot(cx + 0.4, cy + 1.7); let a3 = t - 5.0; let f3 = a3*2.0;
let e3 = step(0.0, a3) * exp(-0.55*a3) * sin(7.0*(d3 - f3)) * exp(-3.0*(d3-f3)*(d3-f3));
// column height = calm water level + the summed latency displacement (always > 0)
let bh = clamp(0.22 + 0.13*pings + 0.17*(e1 + e2 + e3), 0.03, 0.78);
let box = sdbox3(lx, ly, z - bh*0.5, rp*0.42, rp*0.42, bh*0.5);
let d = box;
// hit colour: deep-blue troughs → bright cyan crests (from the actual hit height),
// top faces brightest — no cross-stage lets, so the field colours cleanly
let crest = clamp(hz * 1.7, 0.0, 1.0);
let hue = mod(210.0 - crest*56.0, 360.0);
let sat = 0.82;
let val = 0.16 + 0.55*crest + 0.30*nz;
}
// ---- annotations ----
caption(head, "Docker daemon socket latency", (640, 60), 33);
caption(sub, "each socket ping ripples a raymarched liquid mesh", (640, 112), 21);
hidden(head);
hidden(sub);
equation(eq, (640, 636), `z_{\text{cell}} = \mathrm{water} + \sum_i \mathrm{RTT}_i\,\mathrm{ring}(r_i - c\,\Delta t_i)`, 26);
caption(note, "baked latency trace → SDF displacement → raymarch · pure in t, so it scrubs", (640, 690), 18);
hidden(eq);
hidden(note);
show(head);
wait(1.6);
show(sub);
wait(2.2);
show(eq);
show(note);
// slow orbit so the mesh reads as genuine 3-D geometry
orbit3(52, 8, 4.8, 22, smooth);
ssl-handshake-sdf
A TLS 1.3 handshake rendered as a raymarched SDF scene, message by message. Four scene
parameters are the reactive shader variables the timeline drives: entropy boils every surface,
x25519 key agreement smin-FUSES the two endpoint solids into a secret neither side ever sent, HKDF
sprouts a key TREE, and the encrypted channel becomes a rep()-tiled tunnel of cipher rings. All
ALL-SCALAR (branch tilts as component rotations, no vec2/rot2) so the marcher runs on the GPU.
Honest by design: pure in t, a deterministic replay of one captured handshake — not a live socket.
// ssl-handshake-sdf — a TLS 1.3 handshake rendered as a raymarched SDF scene.
// Four scene parameters are the "reactive shader variables"; the timeline
// replays the handshake by driving them, and the SDF reads them by name:
// ent - cryptographic entropy -> surface displacement (the boil)
// mrg - x25519 key agreement -> the two endpoint solids smin-FUSE
// grw - HKDF key-schedule -> a key TREE grows from the shared secret
// tun - the encrypted channel -> rep()-tiled cipher rings, an endless tunnel
// Honesty note: manic is a pure function of t - no live sockets. This is a
// deterministic replay of one captured handshake; the hex in the transcript is
// that capture, not live traffic. The marcher is fully GPU-transpilable (scalar
// SDF, no vec lets), so it previews and records smoothly.
title("The Handshake, Made Visible");
canvas("16:9");
template("black");
// ---------- HUD ----------
text(head, (cx, 60), "The Handshake, Made Visible"); display(head); cursor(head);
text(cap, (cx, h - 42), ""); size(cap, 26);
equation(eqk, (cx, 170), `(g^{a})^{b} \;=\; (g^{b})^{a}`, 36); hidden(eqk);
// the transcript, typed line by line (terminal-green, top left)
text(tl1, (330, 150), "ClientHello random: 9f3a c241 77d0 8e5b"); size(tl1, 24); color(tl1, lime); cursor(tl1); hidden(tl1);
text(tl2, (330, 190), "ServerHello random: 4be7 01cc a913 f2d6"); size(tl2, 24); color(tl2, lime); cursor(tl2); hidden(tl2);
text(tl3, (330, 230), "KeyShare x25519: e5a2 39f8 1b44 c07e"); size(tl3, 24); color(tl3, lime); cursor(tl3); hidden(tl3);
text(tl4, (330, 270), "HKDF-Expand client + server traffic keys"); size(tl4, 24); color(tl4, lime); cursor(tl4); hidden(tl4);
text(tl5, (330, 310), "Finished cipher: CHACHA20-POLY1305"); size(tl5, 24); color(tl5, lime); cursor(tl5); hidden(tl5);
// entropy readouts (the visible reactive variable + a bit counter)
counter(bits, (w - 185, 118), 0, 0, "entropy bits ", ""); color(bits, gold); hidden(bits);
parameter(ent, (w - 180, 168), 0.02, 0, 1, "entropy", 2);
parameter(mrg, (w - 180, 238), 0, 0, 1, "key-mix", 2); hidden(mrg);
parameter(grw, (w - 180, 308), 0, 0, 1, "hkdf", 2); hidden(grw);
parameter(tun, (w - 180, 378), 0, 0, 1, "tunnel", 2); hidden(tun);
// ---------- the scene ----------
camera3((4.5, -5.5, 3.4), (0, 0, 1.1), 44);
raymarch(hs) {
// entropy boils every surface: high-frequency displacement scaled by `ent`
let wob = ent * 0.13 * sin(6*x + 2*t) * sin(6*y + 1.7*t) * sin(6*z + 1.3*t);
// the two endpoints: browser (sphere) and server (octahedron), pulled
// together as the key-mix parameter rises
let ox = 1.25 - 0.95*mrg;
let c = sdsphere(x + ox, y, z - 0.85, 0.55) + wob;
let s = sdoctahedron(x - ox, y, z - 0.85, 0.62) + wob;
let duo = smin(c, s, 0.12 + 0.55*mrg);
// the HKDF key tree: trunk + branches, growing out of the fused secret as `grw`
// rises. Branch tilts are SCALAR component rotations (not vec2/rot2) so the whole
// marcher transpiles to GLSL and runs on the GPU instead of the heavy CPU fallback.
// rot(a,b,θ) = (a·cosθ − b·sinθ, a·sinθ + b·cosθ)
let zt = z - 0.85;
let tr = sdcapsule(x, y, zt, 1.05*grw, 0.12);
let za = zt - 1.0*grw; let ca = cos(0.65); let sa = sin(0.65);
let zb = zt - 1.55*grw; let cb = cos(0.8); let sb = sin(0.8);
let b1 = sdcapsule(x*ca - za*sa, y, x*sa + za*ca, 0.7*grw, 0.085);
let b2 = sdcapsule(x*ca + za*sa, y, 0 - x*sa + za*ca, 0.7*grw, 0.085);
let b3 = sdcapsule(x, y*cb - zb*sb, y*sb + zb*cb, 0.5*grw, 0.06);
let b4 = sdcapsule(x, y*cb + zb*sb, 0 - y*sb + zb*cb, 0.5*grw, 0.06);
let tree0 = smin(smin(tr, smin(b1, b2, 0.1), 0.12), smin(b3, b4, 0.1), 0.12);
let tree = tree0 + 0.6*wob + (1 - smoothstep(0.02, 0.12, grw))*9;
// the encrypted tunnel: an endless procession of cipher rings along y
let ry = rep(y, 1.05);
let ring = sdtorus(x, z - 0.85, ry, 0.8, 0.05 + 0.03*sin(3*t + y)) + (1 - smoothstep(0.02, 0.2, tun))*9;
let d = smin(smin(duo, tree, 0.14), ring, 0.1);
// colour: cool protocol teal, warmed and destabilized by entropy
let hue = 165 + 55*nz + 30*sin(2*hz + 0.5*t) + 50*ent;
let sat = 0.7;
let val = 0.85 + 0.15*nz;
let alpha = 1;
}
// ================= timeline: the handshake, message by message ==============
type(head, 1.1);
say(cap, "a TLS 1.3 handshake - captured once, replayed as geometry", 0.6);
wait(0.8);
say(cap, "two strangers: your browser, and a server it has never met", 0.5);
wait(1.4);
// ---- ClientHello: 32 bytes of randomness leave home
cue(tick);
show(tl1, 0.1); type(tl1, 1.0);
show(bits, 0.3);
say(cap, "ClientHello: 32 bytes of pure randomness leave home - the surface begins to boil", 0.5);
par { to(ent, value, 0.55, 1.6, smooth); to(bits, value, 256, 1.6); orbit3(-35, 20, 7.4, 1.6, smooth); }
wait(0.8);
// ---- ServerHello: chaos answers chaos
cue(tick);
show(tl2, 0.1); type(tl2, 1.0);
say(cap, "ServerHello: the server answers with chaos of its own", 0.5);
par { to(ent, value, 0.85, 1.4, smooth); to(bits, value, 512, 1.4); }
wait(0.8);
// ---- x25519: the fusion - a secret neither of them ever sent
cue(whoosh);
show(tl3, 0.1); type(tl3, 1.0);
show(eqk, 0.6);
say(cap, "x25519: the shapes fuse into a secret that NEITHER side ever transmitted", 0.5);
par { to(mrg, value, 1, 2.4, smooth); orbit3(15, 24, 6.8, 2.4, smooth); }
wait(0.9);
// ---- HKDF: the key tree
cue(pop);
show(tl4, 0.1); type(tl4, 1.0);
say(cap, "HKDF: one shared secret sprouts a whole tree of session keys", 0.5);
par { to(grw, value, 1, 2.6, smooth); orbit3(60, 26, 6.6, 2.6, smooth); }
wait(0.9);
// ---- Finished: the tunnel opens
cue(chime);
show(tl5, 0.1); type(tl5, 1.0);
say(cap, "Finished: from here on, every byte travels dressed in noise", 0.5);
par { to(tun, value, 1, 2.2, smooth); to(ent, value, 1, 2.2, smooth); orbit3(110, 18, 7.2, 4.5, smooth); }
disintegrate(eqk, 0.8);
say(cap, "the invisible negotiation, rendered visible", 0.6);
wait(2.5);
type-to-sdf
Typography, dismantled across all three shader tiers in one scene. Act 1 — 2,600 points born
INSIDE the glyphs of “SDF” via cloud(…) from text("SDF"), swarming out and flying home. Act 2 —
the letters re-authored as ray-marched signed-distance VOLUMES (raymarch, kept ALL-SCALAR so it
transpiles to the GPU — tori carved by box intersections, no vec2/rot2). Act 3 — the SAME
three sliders (weight/wave/melt) drive a raw glsl pass. One interface, three ways manic renders a
field: per-point cloud, per-pixel scalar raymarch, and hand-written GLSL.
// type-to-sdf — flat 2D typography dismantled into a point field, rebuilt as
// reactive 3D signed-distance volumes, then handed to raw GLSL.
// act 1 cloud ... from text("SDF") - the glyphs dissolve into positions
// act 2 raymarch, ALL-SCALAR - S, D, F re-authored as SDF volumes
// (scalar-only => GPU transpile; parameters bind as uniforms;
// tori are carved with box INTERSECTIONS, no rot2/vec2 anywhere)
// act 3 glsl - the SAME sliders drive raw GLSL
// Three UI parameters are the whole interface: weight / wave / melt.
title("Type, Dismantled");
canvas("16:9");
template("black");
// ---------- HUD ----------
text(head, (cx, 60), "Type, Dismantled: Flat Glyphs to SDF Volumes"); display(head); cursor(head);
text(cap, (cx, h - 42), ""); size(cap, 26);
// the UI: three sliders, visibly driving everything
parameter(wgt, (w - 180, 150), 0.15, 0, 1, "weight", 2);
parameter(wav, (w - 180, 220), 0, 0, 1, "wave", 2);
parameter(mlt, (w - 180, 290), 0, 0, 1, "melt", 2);
// ---------- act 1: the glyph field ----------
// 2600 points born INSIDE the glyphs of "SDF"; each remembers home (hx, hy).
// They swarm out at t~3.2 and fly home at t~6 - the word dismantled and recalled.
cloud(dust, 2600, #ffffff, 0.9) from text("SDF") {
let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
let sc = smoothstep(3.2, 4.6, t) - smoothstep(6.0, 7.6, t);
let ang = i * 2.399;
let rad = 40 + 230 * rn;
let x = hx + sc * rad * cos(ang + 0.5 * t);
let y = hy + sc * rad * sin(ang + 0.5 * t) * 0.8;
let r = 1.6 + 1.4 * rn;
let hue = 190 + 50 * sin(0.7 * i + t);
}
// ---------- act 2: the volumes ----------
camera3((0, -4.6, 1.6), (0, 0, 0.95), 40);
raymarch(vol) {
// domain warp: the WAVE slider bends the coordinate field itself
let xw = x + wav * 0.16 * sin(2.6 * z + 2 * t);
let zw = z + wav * 0.10 * sin(2.2 * x - 1.6 * t);
// ---- S (at x = -1.5): two torus arcs, quadrants removed by intersection
let sx = xw + 1.5;
let torU = sdtorus(sx, zw - 1.15, y, 0.26, 0.09);
let torL = sdtorus(sx, zw - 0.63, y, 0.26, 0.09);
let upC = smin(sdint(torU, sdbox3(sx + 0.30, y, zw - 1.15, 0.32, 0.6, 0.45)), sdint(torU, sdbox3(sx, y, zw - 1.32, 0.6, 0.6, 0.20)), 0.02);
let loC = smin(sdint(torL, sdbox3(sx - 0.30, y, zw - 0.63, 0.32, 0.6, 0.45)), sdint(torL, sdbox3(sx, y, zw - 0.44, 0.6, 0.6, 0.20)), 0.02);
let dS = smin(upC, loC, 0.05);
// ---- D (at x = 0): stem + right half of a ring
let stemD = sdbox3(xw + 0.26, y, zw - 0.9, 0.09, 0.10, 0.55);
let bowl = sdint(sdtorus(xw + 0.10, zw - 0.9, y, 0.36, 0.09), sdbox3(xw - 0.22, y, zw - 0.9, 0.34, 0.6, 0.6));
let dD = smin(stemD, bowl, 0.05);
// ---- F (at x = +1.5): stem + two arms
let fx = xw - 1.5;
let stemF = sdbox3(fx + 0.22, y, zw - 0.9, 0.09, 0.10, 0.55);
let armT = sdbox3(fx, y, zw - 1.36, 0.30, 0.10, 0.09);
let armM = sdbox3(fx - 0.03, y, zw - 0.98, 0.24, 0.10, 0.08);
let dF = smin(stemF, smin(armT, armM, 0.03), 0.05);
// ---- the word: MELT widens the union until letterforms dissolve;
// WEIGHT is one sdround inflation - bold is literally deeper
let k = 0.05 + 0.5 * mlt;
let word = smin(smin(dS, dD, k), dF, k);
let fluid = mlt * 0.03 * sin(5 * xw + 2.4 * t) * sin(4 * zw - 1.8 * t);
let d = sdround(word, 0.02 + 0.09 * wgt) + fluid;
let hue = 190 + 40 * nx + 50 * mlt + 20 * sin(3 * hz + t);
let sat = 0.75;
let val = 0.9 + 0.1 * nz;
let alpha = 1;
}
hidden(vol);
// ---------- act 3: the same sliders, raw GLSL ----------
glsl(neon, `
uniform float u_wgt;
uniform float u_mlt;
float smin2(float a, float b, float k){ float h = clamp(0.5 + 0.5*(b - a)/k, 0.0, 1.0); return mix(b, a, h) - k*h*(1.0 - h); }
void mainImage(out vec4 O, in vec2 I){
vec2 p = (2.0*I - iResolution.xy)/iResolution.y;
float k = 0.15 + 0.6*u_mlt;
float d = 1e5;
for (int i = 0; i < 3; i++) {
float fi = float(i);
vec2 c = vec2(-0.9 + 0.9*fi, 0.12*sin(iTime*1.3 + fi*2.1));
float r = 0.26 + 0.12*u_wgt + 0.05*sin(iTime*2.0 + fi);
d = smin2(d, length(p - c) - r, k);
}
float glow = pow(0.02/max(abs(d), 0.004), 0.9);
vec3 col = glow * mix(vec3(0.1, 0.8, 1.0), vec3(1.0, 0.4, 0.9), 0.5 + 0.5*sin(3.0*u_mlt + p.x));
O = vec4(col, clamp(glow, 0.0, 1.0));
}
`);
hidden(neon);
// ================= timeline =================
// ---- act 1: flat, then dismantled
type(head, 1.1);
say(cap, "this is type as your screen stores it: flat outlines, frozen in place", 0.6);
wait(1.4);
cue(whoosh);
say(cap, "step one: DISMANTLE - a glyph is nothing but a field of positions", 0.5);
wait(2.6);
say(cap, "...and every position remembers home", 0.5);
wait(2.6);
// ---- act 2: rebuilt as signed-distance volumes
cue(whoosh);
par { fade(dust, 0.9); show(vol, 1.2); }
say(cap, "step two: REBUILD - each letter re-authored as a signed distance volume", 0.5);
orbit3(-24, 16, 5.0, 2.0, smooth);
// weight: variable font, one number
cue(tick);
say(cap, "one slider inflates every stroke: WEIGHT - bold is literally deeper", 0.5);
to(wgt, value, 1, 1.6, smooth);
to(wgt, value, 0.35, 1.2, smooth);
// wave: the coordinate field bends
cue(tick);
say(cap, "WAVE bends the coordinate field itself - the letters ride it", 0.5);
par { to(wav, value, 1, 1.8, smooth); orbit3(18, 22, 4.8, 1.8, smooth); }
to(wav, value, 0.35, 1.2, smooth);
// melt: typography as fluid
cue(pop);
say(cap, "MELT widens the union - and the word forgets its letterforms", 0.5);
par { to(mlt, value, 1, 2.6, smooth); orbit3(40, 14, 5.2, 2.6, smooth); }
wait(1.0);
// ---- act 3: the bridge to raw GLSL
cue(chime);
say(cap, "step three: the SAME sliders, handed to raw GLSL at full resolution", 0.5);
par { fade(vol, 1.0); show(neon, 1.0); }
to(mlt, value, 0.25, 1.6, smooth);
par { to(mlt, value, 0.9, 1.8, smooth); to(wgt, value, 0.8, 1.8, smooth); }
to(wgt, value, 0.3, 1.4, smooth);
say(cap, "flat vectors in - living volumes out", 0.6);
wait(2.5);
raymarch-shapes3
The 3-D SDF PRIMITIVES in a real ray-march: a torus, an octahedron and a capsule spindle —
sdtorus/sdoctahedron/sdcapsule — smooth-unioned and orbited by camera3. Each is ONE scalar
builtin (no hand-written distance functions), full-resolution GLSL with the CPU marcher as the exact
fallback.
// raymarch-shapes3 — the 3-D shape/SDF builtins (Layer 1, batch 2) in a real
// ray-march: a torus, an octahedron, and a capsule spindle, smooth-unioned and
// orbited by camera3. Each is ONE scalar builtin — no vec math, no hand-written
// distance functions. Full-res GLSL with the CPU marcher as the exact fallback.
//
// manic examples/raymarch-shapes3.manic
title("Ray-marched SDF primitives — torus · octahedron · capsule");
canvas("16:9");
template("black");
camera3((3.2, -3.6, 2.4), (0, 0, 0), 40, perspective);
raymarch(scene) {
let tor = sdtorus(x, y, z, 0.95, 0.26); // ring in the XY plane
let oct = sdoctahedron(x, y, z - 1.05, 0.5); // floating above
let spn = sdcapsule(x, y, z, 1.5, 0.10); // vertical spindle through it
let d = smin(smin(tor, oct, 0.18), spn, 0.12);
// iridescent hit colour from the normal + a slow time sweep
let hue = mod(190.0 + nz * 70.0 + nx * 40.0 + t * 30.0, 360.0);
let sat = 0.82;
let val = 0.52 + 0.32 * nz;
}
caption(head, "sdtorus · sdoctahedron · sdcapsule", (640, 66), 30);
caption(sub, "each shape one scalar builtin — no vec math", (640, 118), 20);
hidden(head);
hidden(sub);
show(head);
wait(1.4);
show(sub);
orbit3(70, 0, 5.4, 20, smooth);
raymarch-alpha
OUTPUT ALPHA / compositing: a raymarched metaball with let alpha renders on a TRANSPARENT background,
so the object floats over the rest of the scene (here a full-canvas shader gradient) instead of an
opaque backdrop. Faithful on both backends — GLSL writes vec4(rgb, alpha), the CPU field stores
per-texel alpha and composites the same way.
// raymarch-alpha — Shader backbone step 7: OUTPUT ALPHA. A raymarched object with
// `let alpha` renders on a TRANSPARENT background, so it composites over the rest of
// the scene instead of drawing an opaque backdrop. Here a coloured metaball floats
// over a full-canvas 2-D `shader` gradient — you can see the gradient THROUGH the
// object's missed rays (and faintly through the object itself at alpha 0.9). Alpha
// is faithful on both backends: GLSL writes `vec4(rgb, alpha)` and blends; the CPU
// field stores per-texel alpha and composites the same way.
//
// manic examples/raymarch-alpha.manic
title("Raymarch with alpha — compositing over the scene");
canvas("16:9");
template("black");
// a colourful 2-D shader gradient BEHIND the object (fills the canvas, opaque)
shader(back) {
let hue = mod(u * 160.0 + v * 90.0 + 20.0, 360.0);
let sat = 0.7;
let val = 0.42 + 0.12 * sin(u * 6.28);
}
camera3((2.6, -3.2, 2.0), (0, 0, 0), 40, perspective);
raymarch(blob) {
// two spheres smooth-unioned into a metaball
let a = sdsphere(x - 0.55 * sin(t), y, z + 0.4 * cos(t), 0.52);
let b = sdsphere(x + 0.55 * cos(t * 0.8), y - 0.25, z, 0.44);
let d = smin(a, b, 0.4);
// iridescent hit colour
let hue = mod(205.0 + nx * 85.0 + t * 34.0, 360.0);
let sat = 0.82;
let val = 0.55 + 0.3 * nz;
// OUTPUT ALPHA: hit ⇒ 0.9 (slightly translucent), miss ⇒ transparent (composites)
let alpha = 0.9;
}
caption(head, "raymarch ← alpha (composited)", (640, 66), 32);
hidden(head);
show(head);
orbit3(60, 0, 5.0, 18, smooth);
glsl-raymarch
Shader V3: glsl(id, "shader source") runs a REAL GLSL fragment shader (Shadertoy-style mainImage,
iTime/iResolution/iMouse) straight through the graphics pipeline — so the whole scene is marched
PER PIXEL by the GPU/GL: crisp, anti-aliased, full resolution, and fast. The SAME shader runs on Metal
(Mac), llvmpipe (headless prod, JIT to CPU) and WebGL (browser). This is the path to true Shadertoy
quality in manic — paste a shader, it just runs.
// glsl-raymarch — Shader V3: run a REAL GLSL fragment shader, full resolution.
// `glsl(id, `<shader>`)` hands a Shadertoy-style `mainImage` straight to the GPU
// (macroquad material) — so the whole scene is marched PER PIXEL by the graphics
// pipeline: crisp, anti-aliased, and fast, with `iTime`/`iResolution`/`iMouse`
// uniforms. The same shader runs on Metal (Mac), llvmpipe (headless prod, JIT to
// CPU), and WebGL (browser). This is the path to true Shadertoy quality in manic
// — paste a shader, it just runs.
//
// manic examples/glsl-raymarch.manic
title("A real GLSL raymarch — full resolution");
canvas("16:9");
template("black");
glsl(scene, `
float sdSphere(vec3 p, float r){ return length(p) - r; }
float map(vec3 p){
vec3 q = p;
q.xy = mod(q.xy + 1.0, 2.0) - 1.0; // infinite grid
float bob = 0.18 * sin(iTime*1.6 + p.x*0.8 + p.y*0.7);
return sdSphere(q - vec3(0.0, 0.0, bob), 0.42);
}
vec3 nrm(vec3 p){
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(map(p+e.xyy)-map(p-e.xyy),
map(p+e.yxy)-map(p-e.yxy),
map(p+e.yyx)-map(p-e.yyx)));
}
void mainImage(out vec4 o, in vec2 fc){
vec2 uv = (fc*2.0 - iResolution)/min(iResolution.x, iResolution.y);
vec3 ro = vec3(sin(iTime*0.3)*0.7, cos(iTime*0.25)*0.5, 3.0);
vec3 rd = normalize(vec3(uv, -1.6));
float t = 0.0; float hit = 0.0;
for(int i=0;i<110;i++){
vec3 p = ro + rd*t;
float d = map(p);
if(d < 0.001){ hit = 1.0; break; }
t += d;
if(t > 24.0) break;
}
vec3 col = vec3(0.04, 0.05, 0.08);
if(hit > 0.5){
vec3 p = ro + rd*t;
vec3 n = nrm(p);
float diff = max(dot(n, normalize(vec3(0.6, 0.8, 0.5))), 0.0)*0.7 + 0.3;
vec3 base = 0.5 + 0.5*cos(iTime + p.xyx*0.6 + vec3(0.0, 2.0, 4.0)); // iridescent
col = base * diff;
}
o = vec4(pow(col, vec3(0.4545)), 1.0);
}
`);
caption(head, "A real GLSL raymarch", (640, 66), 34);
caption(sub, "glsl(...) runs a fragment shader per pixel — full res, fast", (640, 122), 22);
hidden(head);
hidden(sub);
show(head);
wait(1.6);
show(sub);
wait(24);
glsl-boxgrid
The ACTUAL Shadertoy (tssSDN — a grid of boxes rippling around a bouncing sphere) that started this
whole thread, running in manic UNCHANGED via glsl. The CPU rebuild (raymarch-boxgrid) traded
resolution for the no-GPU farm; this hands the real fragment shader — DDA cell-stepping raymarch,
face-coloured boxes and all — straight to the graphics pipeline, so it renders PER PIXEL at full
resolution (~0.08s a frame on Mac). Paste a shader, it just runs — same code on Mac, prod and browser.
// glsl-boxgrid — the ACTUAL Shadertoy (tssSDN) that started this whole thread,
// running in manic UNCHANGED via the `glsl` V3 path. Our CPU rebuild
// (examples/raymarch-boxgrid.manic) traded resolution for the no-GPU farm; this
// hands the real fragment shader straight to the graphics pipeline, so it renders
// PER PIXEL at full resolution — GPU-marched on Mac, JIT-to-CPU on llvmpipe prod,
// WebGL in the browser. Same shader, three targets. This IS capture.webm.
//
// Original shader: https://www.shadertoy.com/view/tssSDN (ukeyshima)
// manic examples/glsl-boxgrid.manic
canvas("16:9");
template("black");
glsl(scene, `
#define MAX_DIST 1000.
#define SURF_DIST .0001
#define EPS .0001
#define PI 3.141592
#define PI2 PI*2.
#define REP(p,r) mod(p,r) - r * .5
float rand(vec2 co){ return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453); }
float sdSphere(vec3 p, float s){ return length(p) - s; }
float sdBox(vec3 p, vec3 b){ vec3 q = abs(p) - b; return length(max(q, 0.)) + min(max(q.x, max(q.y, q.z)), 0.); }
vec2 minMat(vec2 d1, vec2 d2){ return (d1.x < d2.x) ? d1 : d2; }
float rep = .04;
vec2 scene(vec3 p){
vec2 d = vec2(100000., 0.);
float t = iTime;
vec3 q = p;
vec3 spo = vec3(sin(t * 1.8) * .25, .32, cos(t * 2.2) * .3);
vec3 sp = q - spo;
d.x = sdSphere(sp, .075);
vec2 id = floor(q.xz / rep);
float hash = rand(id * .001);
q.xz = mod(q.xz, rep) - rep * .5;
vec3 bcp = vec3(0.);
bcp.xz = id * rep + rep * .5;
float bsDist = length(spo.xz - bcp.xz);
float s = smoothstep(0., .5, bsDist);
q -= vec3(0., .125 - (sin(hash * PI2 + t * (2. + bsDist * .015)) * .05) * (1. - pow(s, .9)), 0.);
d = minMat(d, vec2(sdBox(q, vec3(rep * .5, .1, rep * .5)), 1.));
return d;
}
vec3 getNormal(vec3 p){
vec2 e = vec2(EPS, 0.);
return normalize(vec3(scene(p + e.xyy).x - scene(p - e.xyy).x,
scene(p + e.yxy).x - scene(p - e.yxy).x,
scene(p + e.yyx).x - scene(p - e.yyx).x));
}
vec2 raymarch(vec3 ro, vec3 rd, float side){
float accDist = 0.; float mat = 0.;
for(int i = 0; i < 128; i++){
vec3 p = ro + rd * accDist;
vec2 result = scene(p);
float dist = result.x * side;
vec3 rdi = 1. / rd;
mat = result.y;
if(abs(dist) < SURF_DIST || accDist > MAX_DIST) break;
accDist += min(min((step(0., rd.x) - mod(p.x, rep)) * rdi.x,
(step(0., rd.z) - mod(p.z, rep)) * rdi.z) + .0001, dist);
}
return vec2(accDist, mat);
}
vec3 getRayDir(vec2 uv, vec3 p, vec3 l, float z){
vec3 forward = normalize(l - p);
vec3 right = normalize(cross(forward, vec3(0., 1., 0.)));
vec3 up = normalize(cross(right, forward));
return normalize(right * uv.x + up * uv.y + forward * z);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
float t = iTime;
vec2 uv = (fragCoord.xy * 2. - iResolution.xy) / min(iResolution.x, iResolution.y);
vec3 ro = vec3(1., 1., 1.2);
vec3 ta = vec3(0., .2, 0.);
vec3 rd = getRayDir(uv, ro, ta, 3.5);
vec2 result = raymarch(ro, rd, 1.);
float dist = result.x; float mat = result.y;
vec3 col = vec3(0.);
if(dist < MAX_DIST){
vec3 p = ro + rd * dist;
vec3 l = normalize(vec3(1., 1., -1.));
vec3 n = getNormal(p);
float diffuse = dot(l, n) * .5 + .5;
vec3 diffuseColor = vec3(diffuse);
if(mat < .5){ diffuseColor *= vec3(1., 0., 0.); }
else {
diffuseColor *= vec3(1.);
if(n.x > .5) diffuseColor = diffuse * vec3(1., 0., 0.);
if(n.y > .5) diffuseColor = diffuse * vec3(1., .9, .9);
if(n.z > .5) diffuseColor = diffuse * vec3(.6, 0., 0.);
}
col = diffuseColor;
}
col = pow(col, vec3(.4545));
fragColor = vec4(col, 1.);
}
`);
wait(12);
glsl-shapes
Patricio Gonzalez Vivo’s 2015 ‘shapes’ Shadertoy running in manic UNCHANGED via glsl — a 2-D SDF
gallery (pentagon/circle/triangle/octagon/hexagon/star/ellipse/rounded-rect) banded by an oscillating
iso-line. Paste the real fragment shader, it just runs at full resolution, animated by iTime. The
DSL twin (shader-shapes-dsl) shows the other path to the same look.
// glsl-shapes — Patricio Gonzalez Vivo's 2015 "shapes" Shadertoy, running in manic
// UNCHANGED via the raw glsl() V3 path (2-D SDF gallery: pentagon/sphere/triangle/
// octagon/hexagon/star/ellipse/rounded-rect, banded by an oscillating iso-line).
// Original: http://patriciogonzalezvivo.com · @patriciogv
canvas("16:9");
template("black");
glsl(scene, `
float osc(float d){
return floor(mod((d+iTime*2.600)/2.,1.)*2.);
}
float sphere(vec2 p, float r){
return length(p)-r;
}
float tri( in vec2 p, in float r ){
const float k = sqrt(3.0);
p.x = abs(p.x) - r;
p.y = p.y + r/k;
if( p.x+k*p.y>0.0 ) p = vec2(p.x-k*p.y,-k*p.x-p.y)/2.0;
p.x -= clamp( p.x, -2.0*r, 0.0 );
return -length(p)*sign(p.y);
}
float pent( in vec2 p, in float r ){
const vec3 k = vec3(0.809016994,0.587785252,0.726542528);
p.x = abs(p.x);
p -= 2.0*min(dot(vec2(-k.x,k.y),p),0.0)*vec2(-k.x,k.y);
p -= 2.0*min(dot(vec2( k.x,k.y),p),0.0)*vec2( k.x,k.y);
p -= vec2(clamp(p.x,-r*k.z,r*k.z),r);
return length(p)*sign(p.y);
}
float oct( in vec2 p, in float r ){
const vec3 k = vec3(-0.9238795325, 0.3826834323, 0.4142135623 );
p = abs(p);
p -= 2.0*min(dot(vec2( k.x,k.y),p),0.0)*vec2( k.x,k.y);
p -= 2.0*min(dot(vec2(-k.x,k.y),p),0.0)*vec2(-k.x,k.y);
p -= vec2(clamp(p.x, -k.z*r, k.z*r), r);
return length(p)*sign(p.y);
}
float hex( in vec2 p, in float r ){
const vec4 k = vec4(-0.5,0.8660254038,0.5773502692,1.7320508076);
p = abs(p);
p -= 2.0*min(dot(k.xy,p),0.0)*k.xy;
p -= 2.0*min(dot(k.yx,p),0.0)*k.yx;
p -= vec2(clamp(p.x,r*k.z,r*k.w),r);
return length(p)*sign(p.y);
}
float str(in vec2 p, in float r, in float rf){
const vec2 k1 = vec2(0.809016994375, -0.587785252292);
const vec2 k2 = vec2(-k1.x,k1.y);
p.x = abs(p.x);
p -= 2.0*max(dot(k1,p),0.0)*k1;
p -= 2.0*max(dot(k2,p),0.0)*k2;
p.x = abs(p.x);
p.y -= r;
vec2 ba = rf*vec2(-k1.y,k1.x) - vec2(0,1);
float h = clamp( dot(p,ba)/dot(ba,ba), 0.0, r );
return length(p-ba*h) * sign(p.y*ba.x-p.x*ba.y);
}
float elp( in vec2 p, in vec2 ab ){
p = abs(p); if( p.x > p.y ) {p=p.yx;ab=ab.yx;}
float l = ab.y*ab.y - ab.x*ab.x;
float m = ab.x*p.x/l; float m2 = m*m;
float n = ab.y*p.y/l; float n2 = n*n;
float c = (m2+n2-1.0)/3.0; float c3 = c*c*c;
float q = c3 + m2*n2*2.0;
float d = c3 + m2*n2;
float g = m + m*n2;
float co;
if( d<0.0 ){
float h = acos(q/c3)/3.0;
float s = cos(h);
float t = sin(h)*sqrt(3.0);
float rx = sqrt( -c*(s + t + 2.0) + m2 );
float ry = sqrt( -c*(s - t + 2.0) + m2 );
co = (ry+sign(l)*rx+abs(g)/(rx*ry)- m)/2.0;
} else {
float h = 2.0*m*n*sqrt( d );
float s = sign(q+h)*pow(abs(q+h), 1.0/3.0);
float u = sign(q-h)*pow(abs(q-h), 1.0/3.0);
float rx = -s - u - c*4.0 + 2.0*m2;
float ry = (s - u)*sqrt(3.0);
float rm = sqrt( rx*rx + ry*ry );
co = (ry/sqrt(rm-rx)+2.0*g/rm-m)/2.0;
}
vec2 r = ab * vec2(co, sqrt(1.0-co*co));
return length(r-p) * sign(p.y-r.y);
}
float rnd( in vec2 p, in float w, in float r ){
p = abs(p);
return length(p-min(p.x+p.y,w)*0.5) - r;
}
vec2 cent = vec2(0.480,0.480);
float line = 0.008;
float spac = 0.0;
void mainImage( out vec4 fragColor, in vec2 fragCoord ){
vec2 st = fragCoord/iResolution.xy * vec2( iResolution.x/iResolution.y, 1. );
vec3 col = vec3(1.0);
float d = pent(st-cent,0.076);
d = min(d,sphere(st-vec2(0.750,0.800),0.076));
d = min(d,tri(st-vec2(0.210,0.790),0.076));
d = min(d,oct(st-vec2(0.160,0.260),0.076));
d = min(d,hex(st-vec2(0.200,0.500),0.036));
d = min(d,str(st-vec2(0.790,0.510),0.108,0.640));
d = min(d,elp(st-vec2(0.630,0.170),vec2(0.280,0.100)));
d = min(d,rnd(st-vec2(0.480,0.790),0.284,0.032));
float band = osc(d*200.*0.288);
if(d > spac && d < spac+line){
col = vec3(1.);
} else if(d > 0.0){
col*= vec3(1.000,0.684,0.364)*(1.-band)+vec3(1.000,0.514,0.128)*band;
} else {
col*= vec3(0.431,0.436,1.000)*(1.-band)+vec3(0.270,0.190,1.000)*band;
}
fragColor = vec4(col,1.0);
}
`);
wait(8);
glsl-parameter
A RAW GLSL shader driven by a scene parameter: declare uniform float u_freq; in the paste and it
AUTO-BINDS to the slider freq, so a raw Shadertoy gets a manic control with no wrapper changes.
Same uniform table the DSL paths use — declaring the camera basis uniform vec3 iCamEye; binds to
camera3 the same way, so orbit3 can sweep a pasted shader too.
// glsl-parameter — a RAW GLSL shader driven by a scene `parameter`. Step 5 of the
// shader backbone: `glsl()` is now a full citizen of the uniform table, so a paste
// that DECLARES `uniform float u_<name>;` auto-binds to the matching `parameter`
// (and `uniform vec3 iCamEye;` etc. would bind to `camera3`). Here `u_freq` is a
// slider: the same shader re-renders as you sweep it — no re-paste, no edits.
//
// manic examples/glsl-parameter.manic
title("A raw GLSL shader, driven by a slider");
canvas("16:9");
template("black");
parameter(freq, (640, 660), 3, 1, 16, "freq", 0);
glsl(scene, `
uniform float u_freq;
void mainImage(out vec4 o, in vec2 fc){
vec2 uv = fc / iResolution;
vec2 c = uv - 0.5;
c.x *= iResolution.x / iResolution.y; // aspect-correct
float d = length(c);
float rings = 0.5 + 0.5 * sin(d * u_freq * 32.0 - iTime);
vec3 col = vec3(rings, 1.0 - rings, 0.5 + 0.5 * sin(d * u_freq * 8.0));
o = vec4(col, 1.0);
}
`);
caption(head, "glsl ← parameter", (640, 66), 34);
hidden(head);
show(head);
to(freq, value, 16, 6, smooth);
bloom-scope
Multi-pass BLOOM, dissected live. A ‘light scope’ (reticle ring + ticks + core + orbiting emitters +
a pulsing probe) in raw glsl, with the bloom pipeline broken into inspectable passes a view slider
steps through: the crisp scene, the bright-pass (luminance over threshold τ — the ticks vanish), the
gaussian stack (N passes, radius DOUBLING each), and the additive composite. No ping-pong buffers
needed: every emitter is an SDF, so each blurred pass is closed-form exp(-d²/σ²) and the mip ladder
is one constant-bound loop. Five sliders bind straight in as u_<name> uniforms; the flicker beat
shows a hard threshold POP the soft knee fixes.
// bloom-scope — multi-pass bloom, dissected live. A "light scope" (reticle
// ring + ticks + core + three orbiting emitters + one pulsing probe) rendered
// in raw GLSL, with the bloom pipeline broken into inspectable passes:
// view 0 the crisp scene (no bloom)
// view 1 bright-pass: luminance over the threshold tau (ticks vanish!)
// view 2 the gaussian stack: N passes, radius DOUBLING each pass
// view 3 composite: scene + sum of passes (additive - base stays crisp)
// manic has no ping-pong buffers - and doesn't need them here: every emitter
// is an SDF, so each blurred pass exists in closed form exp(-d^2/sigma^2);
// the mip ladder becomes one constant-bound loop. Five UI sliders bind
// straight into the shader as u_<name> uniforms. The flicker beat: the probe
// pulses across tau - hard threshold makes its halo POP; the soft knee fixes it.
title("Bloom, One Pass at a Time");
canvas("16:9");
template("black");
// ---------- HUD ----------
text(head, (cx, 60), "Bloom, One Pass at a Time"); display(head); cursor(head);
text(cap, (cx, h - 42), ""); size(cap, 26);
text(viewlab, (300, h - 100), ""); size(viewlab, 24); color(viewlab, lime);
equation(eqb, (cx, 150), `\text{bloom} \;=\; \sum_{p=0}^{N} w_p\; G_{2^{p}\sigma}\!\big(\max(L-\tau,\,0)\big)`, 32); hidden(eqb);
// the UI: five sliders, bound into the shader as u_glo/u_rad/u_pas/u_kne/u_vew
parameter(glo, (w - 180, 140), 0, 0, 2, "intensity", 2);
parameter(rad, (w - 180, 205), 0.5, 0, 2, "radius", 2);
parameter(pas, (w - 180, 270), 0, 0, 5, "passes", 1);
parameter(kne, (w - 180, 335), 0, 0, 1, "knee", 2);
parameter(vew, (w - 180, 400), 0, 0, 3, "view", 1);
counter(cnt, (w - 185, 462), 0, 0, "active passes ", ""); color(cnt, gold); hidden(cnt);
// ---------- the shader ----------
glsl(scope, `
uniform float u_glo;
uniform float u_rad;
uniform float u_pas;
uniform float u_kne;
uniform float u_vew;
float sdRing(vec2 p, float R){ return abs(length(p) - R); }
float sdBox(vec2 p, vec2 b){ vec2 d = abs(p) - b; return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0); }
// bright-pass weight: hard threshold (tau = 0.55) blended toward a soft knee
float knee(float b){ return mix(step(0.55, b), smoothstep(0.25, 0.70, b), u_kne); }
// the gaussian stack: N passes, sigma doubling each pass, weights decaying -
// the closed-form mip ladder (no texture taps: d comes from the SDF)
vec3 halo(float d, vec3 cb, float w){
vec3 a = vec3(0.0);
for (int pp = 0; pp < 5; pp++) {
float on = clamp(u_pas - float(pp), 0.0, 1.0);
float s = max(0.02, u_rad * 0.05 * pow(2.0, float(pp)));
a += on * w * cb * exp(-(d*d)/(s*s)) * 0.6 / pow(1.6, float(pp));
}
return a * u_glo;
}
void mainImage(out vec4 O, in vec2 I){
vec2 p = (2.0*I - iResolution.xy)/iResolution.y;
// ---- the light scope: reticle ring + folded tick marks + core
float dRing = sdRing(p, 0.62);
vec2 q = vec2(abs(p.x), abs(p.y));
float ticks = min(sdBox(vec2(q.x - 0.62, p.y), vec2(0.055, 0.012)),
sdBox(vec2(p.x, q.y - 0.62), vec2(0.012, 0.055)));
float dCore = length(p) - 0.035;
// ---- three orbiting emitters + the pulsing probe (the flicker demo)
float a1 = 0.7*iTime;
vec2 c1 = 0.45*vec2(cos(a1), sin(a1));
vec2 c2 = 0.45*vec2(cos(a1 + 2.09), sin(a1 + 2.09));
vec2 c3 = 0.45*vec2(cos(a1 + 4.19), sin(a1 + 4.19));
float o1 = length(p - c1) - 0.050;
float o2 = length(p - c2) - 0.040;
float o3 = length(p - c3) - 0.045;
float dPr = length(p - vec2(0.0, -0.86)) - 0.055;
float bPr = 0.35 + 0.45*(0.5 + 0.5*sin(2.6*iTime));
vec3 cRing = vec3(0.30, 0.85, 1.00);
vec3 cCore = vec3(1.00, 0.95, 0.80);
vec3 cO1 = vec3(1.00, 0.72, 0.25);
vec3 cO2 = vec3(1.00, 0.35, 0.80);
vec3 cO3 = vec3(0.30, 1.00, 0.75);
vec3 cPr = vec3(0.75, 0.85, 1.00);
// ---- view 0: the crisp scene
float e = 0.006;
vec3 scene = vec3(0.0);
scene += smoothstep(e, 0.0, dRing) * cRing * 0.77;
scene += smoothstep(e, 0.0, ticks) * cRing * 0.50;
scene += smoothstep(e, 0.0, dCore) * cCore * 1.00;
scene += smoothstep(e, 0.0, o1) * cO1 * 0.95;
scene += smoothstep(e, 0.0, o2) * cO2 * 0.90;
scene += smoothstep(e, 0.0, o3) * cO3 * 0.90;
scene += smoothstep(e, 0.0, dPr) * cPr * bPr;
// ---- view 1: bright extraction (the 0.50 reticle ticks fall BELOW tau)
vec3 bright = vec3(0.0);
bright += smoothstep(e, 0.0, dRing) * cRing * knee(0.77);
bright += smoothstep(e, 0.0, dCore) * cCore * knee(1.00);
bright += smoothstep(e, 0.0, o1) * cO1 * knee(0.95);
bright += smoothstep(e, 0.0, o2) * cO2 * knee(0.90);
bright += smoothstep(e, 0.0, o3) * cO3 * knee(0.90);
bright += smoothstep(e, 0.0, dPr) * cPr * knee(bPr);
// ---- views 2/3: the stacked gaussian passes
vec3 bloom = vec3(0.0);
bloom += halo(dRing, cRing * 0.77, knee(0.77) * 0.55);
bloom += halo(dCore, cCore, knee(1.00));
bloom += halo(o1, cO1 * 0.95, knee(0.95));
bloom += halo(o2, cO2 * 0.90, knee(0.90));
bloom += halo(o3, cO3 * 0.90, knee(0.90));
bloom += halo(dPr, cPr * bPr, knee(bPr));
// ---- the view dial
float vA = 1.0 - clamp(abs(u_vew - 0.0), 0.0, 1.0);
float vB = 1.0 - clamp(abs(u_vew - 1.0), 0.0, 1.0);
float vC = 1.0 - clamp(abs(u_vew - 2.0), 0.0, 1.0);
float vD = 1.0 - clamp(abs(u_vew - 3.0), 0.0, 1.0);
vec3 col = vA*scene + vB*bright + vC*bloom + vD*(scene + bloom);
float aOut = clamp(1.6*max(col.r, max(col.g, col.b)), 0.0, 1.0);
O = vec4(col, aOut);
}
`);
// ================= timeline =================
// ---- view 0: honest and dead
type(head, 1.1);
say(viewlab, "VIEW 0 - scene, no bloom", 0.3);
say(cap, "a light scope, rendered honest: crisp, correct - and completely dead", 0.6);
wait(2.2);
// ---- view 1: the bright-pass
cue(tick);
say(viewlab, "VIEW 1 - bright-pass: L > tau", 0.3);
to(vew, value, 1, 0.8, smooth);
say(cap, "pass one: keep only what outshines the threshold - the fine ticks vanish", 0.5);
wait(2.6);
// ---- view 2: stack the gaussian passes, one by one
cue(whoosh);
say(viewlab, "VIEW 2 - gaussian stack, radius x2 per pass", 0.3);
to(vew, value, 2, 0.8, smooth);
show(eqb, 0.6);
show(cnt, 0.3);
to(glo, value, 1, 0.8, smooth);
say(cap, "now blur it - again and again, radius doubling: the mip ladder", 0.5);
par { to(pas, value, 1, 0.7, smooth); to(cnt, value, 1, 0.7); }
wait(0.4);
par { to(pas, value, 2, 0.7, smooth); to(cnt, value, 2, 0.7); }
wait(0.4);
par { to(pas, value, 3, 0.7, smooth); to(cnt, value, 3, 0.7); }
wait(0.4);
par { to(pas, value, 5, 1.0, smooth); to(cnt, value, 5, 1.0); }
say(cap, "no ping-pong buffers: the emitters are SDFs, so every pass exists in closed form", 0.5);
wait(1.2);
// ---- view 3: the composite
cue(chime);
say(viewlab, "VIEW 3 - composite: scene + all passes", 0.3);
to(vew, value, 3, 0.9, smooth);
say(cap, "add it back on top: the scope begins to BLEED - but the base layer stays crisp", 0.5);
wait(2.0);
// ---- the flicker, and the fix
cue(tick);
say(cap, "watch the probe at the bottom: a HARD threshold makes its halo pop on and off", 0.5);
wait(3.4);
say(cap, "the fix: a soft KNEE - brightness eases across tau, and the halo breathes", 0.5);
to(kne, value, 1, 1.6, smooth);
wait(3.0);
// ---- finale: crank it
cue(whoosh);
say(cap, "clarity amidst chaos: bloom is additive light, never a blur of the scene", 0.5);
par { to(glo, value, 1.7, 2.0, smooth); to(rad, value, 1.5, 2.0, smooth); breathe(eqb, 3, 0.05, 0, 6); }
to(rad, value, 0.9, 1.6, smooth);
say(cap, "five sliders, one shader, every pass inspectable", 0.6);
wait(2.5);
tree-of-leaves
A seed becomes a tree over one year — entirely from cloud particles. A wood front grows the trunk
and branches, then 5,500 golden-angle leaves POP (spring), flutter, turn autumn, FALL in a gust and
fade into the soil, then RETURN green; blossoms open and shed petal-snow. 8,420 points, one small
formula each, every one a pure function of index and time (scrub-safe, no state).
// tree-of-leaves — the full lifecycle cut. One seed, one year, one clock.
// Everything is still a pure function of index i and time t (scrub-safe, no
// state): four clouds now tell a complete story --
// stars (220) - twinkling night sky
// wood (1,500) - trunk + five branches, grown by a front after the seed lands
// leaves (5,500) - golden-angle canopy; each leaf POPS (spring), flutters,
// turns autumn-coloured on its own clock, FALLS in the gust,
// fades into the soil over winter... and RETURNS, green again
// bloom (1,200) - blossoms that open on the young canopy, shed their petals
// in a slow petal-snow, and bloom once more at the end
// 8,420 points - one small formula each.
// Idioms: max(0,s) = 0.5*(s+abs(s)); min(a,b) = a - max(0, a-b);
// hash(i) = mod(abs(sin(i*12.9898)*43758.55), 1)
//
// manic tree-of-leaves.manic
title("A Seed, a Tree, a Year");
canvas("9:16");
template("black");
// ---------- HUD ----------
text(head, (540, 150), "A Seed, a Tree, a Year"); display(head); cursor(head);
text(season, (540, 330), ""); size(season, 34); color(season, lime);
text(cap, (540, 1800), ""); size(cap, 30);
counter(nlv, (540, 255), 0, 0, "leaves ", ""); color(nlv, lime); hidden(nlv);
// ---------- stage ----------
circle(moon, (880, 235), 62); color(moon, #f6e7c0); glow(moon, 20); hidden(moon);
line(gnd, (60, 1668), (1020, 1668)); stroke(gnd, 3); color(gnd, dim); untraced(gnd);
dot(seed, (540, 290), 9); color(seed, gold); glow(seed, 10); hidden(seed);
// ---------- the night sky: 220 twinkling stars ----------
cloud(stars, 220, #ffffff, 0.5) {
let rn = mod(abs(sin(i * 91.17) * 4375.8), 1);
let rn2 = mod(abs(sin(i * 45.7) * 7919.3), 1);
let x = 40 + rn * 1000;
let y = 60 + rn2 * 500;
let tw = 0.5 + 0.5 * sin(2 * t + i * 1.3);
let r = (0.6 + rn * 1.2) * tw * tanh(t) + 0.3;
let hue = 200 + rn * 40;
}
// ---------- the wood: grows out of the planted seed (front starts t~3.6) ----
cloud(wood, 1500, #ffffff, 0.5) {
let p = i / 1500;
let b = mod(i, 5);
let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
let q = 0.5 * ((2 * p - 1) + abs(2 * p - 1));
let pt = 2 * p - q;
let tx = 540 + (b - 2) * 175;
let ty = 800 + abs(b - 2) * 95;
let sx = 540 + 18 * sin(3.1 * pt) + (tx - 540) * q + 12 * sin(3.14159 * q) * (b - 2) * 0.3;
let sy = 1660 - 510 * pt + (ty - 1150) * q;
let wid = 26 * (1 - 0.7 * pt) * (1 - 0.55 * q) + 3;
let jx = (rn - 0.5) * 2 * wid;
let jy = (rn2 - 0.5) * 14;
// wind, with the cold gust that strips the tree near t = 19
let hgt = (1660 - sy) / 900;
let gd = t - 19;
let gust = 1 + 1.3 * exp(-0.4 * gd * gd);
let wind = 14 * sin(0.9 * t + 0.004 * sy) * hgt * hgt * gust;
// growth front: sweeps p = 0..1 starting when the seed has been planted
let sv = tanh((0.3 * (t - 3.6) - p) * 5);
let vfront = 0.5 * (sv + abs(sv));
let x = sx + jx + wind;
let y = sy + jy;
let r = (2.2 + 2.2 * (1 - pt) * (1 - q)) * vfront;
let hue = 22 + rn * 14;
}
// ---------- the leaves: pop, flutter, turn, fall, fade... and RETURN --------
cloud(leaves, 5500, #ffffff, 0.5) {
let b = mod(i, 5);
let k = floor(i / 5);
let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
let tx = 540 + (b - 2) * 175;
let ty = 800 + abs(b - 2) * 95;
let th = k * 2.39996;
let rad = 10.5 * sqrt(mod(k, 210)) + 8 * rn;
let bx = tx + rad * cos(th);
let by = ty + 0.78 * rad * sin(th) - 25;
let px = bx + (540 - bx) * 0.10;
let py = by + (860 - by) * 0.10;
// spring: each leaf unfurls on its own delay (t ~ 6.4 .. 8.8)
let ap0 = tanh((t - 6.4 - 2.4 * rn) * 2.2);
let ap = 0.5 * (ap0 + abs(ap0));
// wind + flutter, gusting near t = 19
let hgt = (1660 - py) / 900;
let gd = t - 19;
let gust = 1 + 1.3 * exp(-0.4 * gd * gd);
let wind = 26 * sin(0.9 * t + 0.004 * py + 2 * rn) * hgt * hgt * gust;
let fl = 4 * sin(2.3 * t + 1.7 * i);
// autumn: green -> gold/red, each leaf at its own pace (t ~ 16 .. 20)
let au0 = (t - 16 - 2.2 * rn) * 0.5;
let au1 = 0.5 * (au0 + abs(au0));
let au = au1 - 0.5 * ((au1 - 1) + abs(au1 - 1));
// the fall: EVERY leaf lets go this time (t ~ 19.5 .. 23), sways down, lands
let s2 = t - 19.5 - 3.5 * rn2;
let dt = 0.5 * (s2 + abs(s2));
let yfree = py + 55 * dt * dt;
let yg = 1665 + 20 * rn;
let yfall = yfree - 0.5 * ((yfree - yg) + abs(yfree - yg));
let sway = 30 * sin(2.2 * dt + i) * tanh(dt) * exp(-0.10 * dt);
// winter: the fallen fade into the soil (t ~ 24.5 .. 27.5)
let go0 = tanh((t - 24.5 - 1.6 * rn) * 1.6);
let gone = 0.5 * (go0 + abs(go0));
// spring again: a NEW leaf opens at the same spot on the branch (t ~ 27 .. 30)
let rb0 = tanh((t - 27 - 2.2 * rn) * 2.0);
let rb = 0.5 * (rb0 + abs(rb0));
// two position tracks, blended: the falling track and the fresh canopy track
let xfall = px + wind * (1 - tanh(2 * dt)) + fl + sway;
let xcan = px + wind + fl;
let x = xfall * (1 - rb) + xcan * rb;
let y = yfall * (1 - rb) + py * rb;
let r = (2.6 + 1.8 * rn2) * (ap * (1 - gone) + rb);
// hue: green, autumn-shifted, reset to green by rebirth
let hgr = 96 + 36 * rn;
let hau = 18 + 34 * rn2;
let hue = hgr + (hau - hgr) * au * (1 - rb);
}
// ---------- the blossoms: open on the young tree, shed petal-snow, return ---
cloud(bloom, 1200, #ffffff, 0.5) {
let b = mod(i, 5);
let k = floor(i / 5);
let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
let tx = 540 + (b - 2) * 175;
let ty = 800 + abs(b - 2) * 95;
let th = k * 2.39996 + 1.3;
let rad = 21 * sqrt(mod(k, 48)) + 6 * rn;
let bx = tx + rad * cos(th);
let by = ty + 0.78 * rad * sin(th) - 25;
let px = bx + (540 - bx) * 0.10;
let py = by + (860 - by) * 0.10;
// first bloom: t ~ 9 .. 11.3
let bp0 = tanh((t - 9 - 1.8 * rn) * 2.4);
let bp = 0.5 * (bp0 + abs(bp0));
// petal-snow: slow drift down from t ~ 12.2, landing softly
let s2 = t - 12.2 - 2.2 * rn2;
let dt = 0.5 * (s2 + abs(s2));
let yfree = py + 16 * dt * dt + 30 * dt;
let yg = 1662 + 22 * rn;
let yfall = yfree - 0.5 * ((yfree - yg) + abs(yfree - yg));
let sway = 40 * sin(1.8 * dt + i) * tanh(dt);
// fallen petals melt away t ~ 18 .. 20.4 (before the leaf carpet arrives)
let go0 = tanh((t - 18 - 1.2 * rn) * 1.8);
let gone = 0.5 * (go0 + abs(go0));
// the second bloom, right at the end: the cycle begins again (t ~ 30.5+)
let rb0 = tanh((t - 30.5 - 1.2 * rn) * 2.6);
let rb = 0.5 * (rb0 + abs(rb0));
let hgt = (1660 - py) / 900;
let wind = 20 * sin(0.9 * t + 0.004 * py + 2 * rn) * hgt * hgt;
let xfall = px + wind * (1 - tanh(2 * dt)) + sway;
let xcan = px + wind + 3 * sin(2.1 * t + i);
let x = xfall * (1 - rb) + xcan * rb;
let y = yfall * (1 - rb) + py * rb;
let tw = 1 + 0.15 * sin(3 * t + i);
let r = (2.2 + 1.6 * rn2) * (bp * (1 - gone) + rb) * tw;
let hue = 318 + 26 * rn;
}
// ================= timeline (narration over the self-evolving year) =========
type(head, 1.1);
par { show(moon, 0.8); draw(gnd, 0.8); }
// ---- the seed
say(cap, "it begins with a single seed", 0.5);
show(seed, 0.3);
shift(seed, (0, 1355), 1.0, in);
cue(pop);
fade(seed, 0.5);
// ---- spring: sprout and first leaves
par { say(cap, "a sprout reaches for the sky", 0.5); say(season, "spring", 0.3); }
wait(2.0);
say(cap, "first leaves unfurl, one by one", 0.5);
show(nlv, 0.3);
to(nlv, value, 5500, 2.6, smooth);
// ---- the flowering
say(cap, "and then - the tree FLOWERS", 0.5);
cue(chime);
wait(1.8);
// ---- summer: petal-snow
par { say(cap, "petals drift away... summer settles in", 0.5); say(season, "summer", 0.3); recolor(season, gold, 0.3); }
wait(3.0);
// ---- autumn
par { say(cap, "autumn arrives, one leaf at a time", 0.5); say(season, "autumn", 0.3); recolor(season, orange, 0.3); }
cue(tick);
wait(2.6);
// ---- the gust: every leaf lets go
say(cap, "a cold wind - and every leaf lets go", 0.5);
cue(whoosh);
to(nlv, value, 0, 4.6, smooth);
// ---- winter
par { say(cap, "winter: the tree remembers in silence", 0.5); say(season, "winter", 0.3); recolor(season, cyan, 0.3); }
wait(2.2);
// ---- spring again
par { say(cap, "...and then, again", 0.5); say(season, "spring, again", 0.3); recolor(season, lime, 0.3); }
cue(whoosh);
to(nlv, value, 5500, 3.0, smooth);
say(cap, "new leaves - and new flowers", 0.5);
cue(chime);
wait(2.6);
// ---- close
say(cap, "a seed, a tree, a year - 8,420 points, one formula each", 0.6);
wait(2.5);
map-attractor
A discrete 2D strange attractor via cloud … from map(...) — the discrete twin of from flow. map
iterates a 2D map (x,y)->(x’,y’) 40,000 times (a stateful recurrence clouds can’t do) and hands each
point the i-th state; the cloud projects it. Covers the Gumowski-Mira / Clifford / de Jong / Hénon
family. @yuruyurau’s idea, hue’d + annotated on a 9:16 Short.
// map-attractor — a discrete 2-D strange attractor, via the new `cloud … from
// map(...)` source: the discrete twin of `from flow`. `map` ITERATES a 2-D map
// (x,y) -> (x',y') 40,000 times (a stateful recurrence clouds alone can't do) and
// hands each point the i-th state as hx/hy; the cloud's formulas project it.
// Covers the whole Gumowski-Mira / Clifford / de Jong / Hénon family. Most maps
// take both formulas from the OLD state (clean); THIS one is sequential
// (y' uses x'), so x' is inlined into y' below.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated,
// bloomed take on a 9:16 Short — pure in (i, t), so it scrubs and records.
//
// manic examples/map-attractor.manic
title("A strange attractor from one map");
canvas("9:16");
template("black");
cloud(atr, 40000, #ffffff, 0.5)
from map("(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))", "-0.8*(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))+3.6*(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))*(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))/(1+(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))*(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x)))-x", (1, 1)) {
// hx, hy = the i-th iterated state; project it through a polar lens
let c = t - hypot(hx, hy)/4;
let px = hy*(5*sin(c) + 11);
let py = hx*(2*cos(c) + 7) + 9*sin(hy/4 + t);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.3 * grow;
let y = 980 + py * 2.3 * grow;
let hue = mod(hypot(hx, hy)*24 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "A strange attractor from one map", (540, 140), 34);
caption(sub, "40,000 iterations, no simulation loop", (540, 208), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `(x,y)\;\to\;(x',\,y')`, 38);
caption(lab, "iterate a discrete map, then project it", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(30);
lorenz-shells
The Lorenz attractor through a different lens, same cloud … from flow(...) bridge (σ=9, ρ=28, β=2):
the integrated state projected via a polar lens fanned into 3 layers into flowing shell forms. One
bridge, endless dynamical-system art. @yuruyurau’s idea, hue’d + annotated.
// lorenz-shells — the Lorenz attractor through a different lens, via the same
// `cloud … from flow(...)` bridge (σ=9, ρ=28, β=2). `flow` integrates the 3D ODE
// and hands each of 30,000 points the i-th state as hx/hy/hz; the cloud projects
// (hx, hz) through a polar lens fanned into 3 layers (mod i,3) into flowing shell
// forms. One bridge, endless dynamical-system art.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated
// take on a 9:16 Short — pure in (i, t), so it scrubs and records.
//
// manic examples/lorenz-shells.manic
title("The Lorenz flow, in shells");
canvas("9:16");
template("black");
cloud(shells, 30000, #ffffff, 0.55)
from flow("9*(y - x)", "x*(28 - z) - y", "x*y - 2*z", (6, 6, 6), 0.001) {
let q = hx*(sin(t*pi/80 - hx*hx/89 + mod(i,3))*0.8 + 1.2)*2 + 99;
let k = hz/39 + t*pi/960 + mod(i,3)*2;
let px = q*sin(k);
let py = q*cos(k);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.2 * grow;
let y = 960 + py * 2.2 * grow;
let hue = mod(mod(i,3)*90 + hz*4 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "The Lorenz flow, in shells", (540, 140), 36);
caption(sub, "integrate chaos, then project it", (540, 208), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `\dot x=\sigma(y{-}x),\;\; \dot y=x(\rho{-}z){-}y,\;\; \dot z=xy-\beta z`, 24);
caption(lab, "same flow as lorenz-attractor, a new lens", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(20);
flow-lorenz
The Lorenz attractor through an artist’s lens, via cloud … from flow(...): flow integrates the
3D Lorenz ODE (a stateful system clouds alone can’t do) and hands each point the i-th state as
hx/hy/hz; the cloud’s formulas project it — here a polar lens fanned into 9 layers. One primitive now
visualises ANY dynamical system through ANY projection. @yuruyurau’s idea, hue’d + annotated.
// flow-lorenz — the Lorenz attractor seen through an artist's lens, via the new
// `cloud … from flow(...)` bridge. `flow` integrates the 3-D Lorenz ODE (a
// STATEFUL system clouds alone can't do); each of 30,000 points receives the
// i-th integrated state as hx/hy/hz, and the cloud's own formulas project that
// state to the screen — here a polar lens fanned into 9 layers (mod(i,9)). So one
// primitive now visualises ANY dynamical system through ANY projection you write.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau); our hue'd, annotated,
// bloomed take on a 9:16 Short. The maths is the real Lorenz flow, not a picture.
//
// manic examples/flow-lorenz.manic
title("A chaotic flow, projected");
canvas("9:16");
template("black");
cloud(art, 30000, #ffffff, 0.55)
from flow("9*(y - x)", "x*(28 - z) - y", "x*y - 2*z", (9, 9, 9), 0.0005) {
// hx, hy, hz = the i-th integrated Lorenz state; project it through the lens
let e = sin(t*pi/20 - hx*hx/99 + mod(i, 9)) + 1;
let q = hx*e + 89;
let k = hz/59 - e/29 + t*pi/480 + mod(i, 9)*8;
let px = q*cos(k);
let py = (q + 60*cos(k/2)) * sin(k);
let grow = tanh(t*0.5 + 0.12); // bloom from the centre
let x = 540 + px * 2.2 * grow;
let y = 960 - py * 2.2 * grow;
let hue = mod(mod(i, 9)*40 + hz*3 + t*14, 360); // colour by layer + depth
}
// ---- textbook annotations ----
caption(head, "A chaotic flow, projected", (540, 140), 36);
caption(sub, "integrate the Lorenz system, then project it", (540, 208), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706),
`\dot x=\sigma(y{-}x),\;\; \dot y=x(\rho{-}z){-}y,\;\; \dot z=xy-\beta z`, 24);
caption(lab, "the state → your own lens, per point", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-phoenix
Another @yuruyurau art-tweet in ONE cloud: 10,000 points flow into a living, wing-like form and
morph over time. The original packs a (y<9?9:5) ternary into the amplitude; manic formulas have no
comparisons, so it’s written with sign(). Hue-gradient coloured and bloomed on a 9:16 Short, pure
in (i, t) so it scrubs — the p5 original can’t.
// cloud-phoenix — another @yuruyurau art-tweet, reimagined in ONE `cloud`. 10,000
// points flow into a living, wing-like form and morph over time. The original
// packs a conditional into the amplitude ((y<9?9:5)); manic formulas have no
// ternary, so it's written with sign(): amp = 7 + 2*sign(9 - s). Coloured per
// point and bloomed from the centre on a 9:16 Short — every point a pure function
// of (i, t), so it scrubs and records (the p5 original can't).
//
// Original idea by @yuruyurau (https://x.com/yuruyurau) — a prolific poster of
// these tiny p5.js/dwitter art formulas. This is our own hue'd, annotated take.
//
// manic examples/cloud-phoenix.manic
title("One formula becomes a phoenix");
canvas("9:16");
template("black");
cloud(bird, 10000, #ffffff, 0.72) {
let s = i / 353; // the reference's "y" parameter
let amp = 7 + 2*sign(9 - s); // (y<9?9:5), written with sign()
let k = (amp + cos(s*31 - t)) * cos(i/44);
let e = s/9 - 14;
let d = hypot(k, e) / 1.6;
let c = d - t/2;
// raw coords (centred at 0), then bloom + scale onto the 1080x1920 frame
let px = (d*9 + k*k)*cos(c);
let py = (55 + d*9)*sin(c/3) + 4*sin(k*2) + s/29*k*(e + 3*sin(e*4 - d*4 + t*3));
let grow = tanh(t * 0.5 + 0.12);
let x = 540 + px * 2.6 * grow;
let y = 960 + (py - 120) * 2.6 * grow; // -120 recentres the form's DC offset
let r = 1.4;
let hue = mod(s * 13 + t * 16, 360); // a rainbow flowing along the form
}
// ---- textbook annotations ----
caption(head, "One formula becomes a phoenix", (540, 132), 38);
caption(sub, "10,000 points, no simulation", (540, 200), 24);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p_i = f(i,\; t)`, 46);
caption(lab, "each point placed by its index i and time t", (540, 1786), 22);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(27);
cloud-pottery
The OpenProcessing weekly ‘Pottery’ challenge in ONE cloud: seven stippled vases, each a UNIQUE
wavy silhouette from a couple of seeded sine harmonics, drawn as dotted ellipse-rings shaded
front-bright by per-dot alpha, with flowering sprigs on a hash-picked subset (a curved stem + a
dotted 5-lobe head). Every dot is a pure function of index and time, so the pots GROW in bottom-up
(tanh(t)), the sprigs sprout, and then the glaze CYCLES through the colour ‘sets’ the p5 original
picks at random — same scene, live, and it scrubs/records exactly. The recursive branching stays with
the original (a flat f(i,t) cloud can’t recurse); the signature look is one formula per point.
// cloud-pottery — the OpenProcessing weekly "Pottery" creative-coding challenge
// (https://openprocessing.org/curation/78544) reimagined in ONE manic `cloud`. The p5
// original stacks random easing curves into pot silhouettes and stipples them with
// thousands of dots across three graphics layers, plus recursive sticks + flowers.
// manic can't recurse in a flat `f(i,t)` cloud — so the sticks/flowers/layers are left
// to the original — but the SIGNATURE look (dotted ellipse-ring vases, each a unique
// wavy profile, shaded front-bright) is one closed-form formula per point. Pure in
// (i,t): the pots grow in bottom-up and record exactly, where the p5 sketch draws once.
//
// manic examples/cloud-pottery.manic
title("Pottery — a p5 sketch, reimagined as one cloud");
canvas("16:9");
template("black");
cloud(pots, 40320, #c47a3d, 0.95) {
// decode the flat index i into (pot, ring, angle-around-the-ellipse)
let per = 5760; // 90 rings * 64 dots
let pot = floor(i / per);
let rem = i - pot * per;
let ring = floor(rem / 64);
let k = rem - ring * 64;
let yl = ring / 89; // 0 = base, 1 = rim
let a = k / 64 * tau;
// 7 pots across a 1280-wide frame (margin 86)
let span = 1108;
let potX = 86 + (pot + 0.5) * (span / 7);
let potW = (span / 7) * 0.4; // half-width
let baseY = 545;
let potH = potW * (1.0 + 1.8 * rand2(pot, 1.7));
// a wavy vase silhouette, unique per pot (a couple of seeded sine harmonics)
let seed = rand2(pot, 3.7);
let prof = 0.55 + 0.28 * sin(yl * pi + seed * tau) + 0.12 * sin(yl * tau + seed * 9.0);
let R = potW * prof;
// ellipse ring, 0.22 vertical squash for perspective, a little per-dot jitter
let x = potX + R * sin(a) + 1.4 * rand2(i, 2.1);
let y = baseY - yl * potH + R * 0.22 * (-cos(a)) + 1.4 * rand2(i, 5.3);
// grow bottom-up: only rings below the rising build-line have appeared
let build = tanh(t * 0.55) * 1.14;
let vis = 1 - smoothstep(build, build + 0.10, yl);
let r = 0.9 + 1.1 * rand2(i, 7.0); // fine stipple
// terracotta while it grows; AFTER the pots are formed the glaze shifts, cycling
// through the "colour sets" the p5 original picks at random — same scene, live.
let cyc = (t - 4.0) * step(4.0, t); // 0 until t=4, then climbs
let hue = mod(26 + cyc * 34.0 + 16 * rand2(pot, 4.0), 360);
let alpha = (0.38 + 0.55 * (0.5 - 0.5 * cos(a))) * vis; // near-side brighter; fades in
}
// flowering sprigs on SOME vessels (the original branches recursively; a flat cloud
// can't, so this is a hash-selected subset with a curved stem + a dotted flower head)
cloud(sprigs, 12000, #ffdd88, 0.92) {
let per = 2000;
let pot = floor(i / per);
let rem = i - pot * per;
let has = step(0.55, rand2(pot, 9.1)); // ~45% of pots get a sprig
// recompute the pot geometry so the sprig sits on the rim
let span = 1108;
let potX = 86 + (pot + 0.5) * (span / 7);
let potW = (span / 7) * 0.4;
let baseY = 545;
let potH = potW * (1.0 + 1.8 * rand2(pot, 1.7));
let rimY = baseY - potH;
let seedF = rand2(pot, 5.9);
let stemLen = potH * (0.8 + 0.7 * seedF);
let lean = (rand2(pot, 2.3) - 0.5) * 80;
let isStem = 1 - step(600, rem); // first 600 dots = stem, rest = head
// stem: a gentle bow from the rim upward
let ts = rem / 600;
let sx = potX + lean * ts + 10 * sin(ts * pi + seedF * 6.0);
let sy = rimY - stemLen * ts;
// flower head at the stem top (ts = 1)
let hx = potX + lean + 10 * sin(pi + seedF * 6.0);
let hy = rimY - stemLen;
let tf = (rem - 600) / 1400;
let fang = tf * tau * 7.0; // spiral fills the head
let petal = 0.5 + 0.5 * abs(sin(fang * 2.5)); // 5-lobe petals
let frad = (14 + 8 * seedF) * petal * (0.35 + 0.65 * rand2(i, 3.3));
let fx = hx + frad * sin(fang) + 3 * (rand2(i, 7.7) - 0.5);
let fy = hy + frad * (-cos(fang)) + 3 * (rand2(i, 8.8) - 0.5);
let x = select(sx, fx, isStem);
let y = select(sy, fy, isStem);
let r = select(1.0 + 0.5 * rand2(i, 1.1), 1.3 + 1.5 * rand2(i, 2.2), isStem);
let hue = select(96, 44, isStem); // stem green, flower gold
let grow = smoothstep(2.6, 4.6, t); // sprigs sprout after the pots form
let alpha = has * grow * (0.4 + 0.55 * rand2(i, 6.6));
}
// ---- textbook annotations ----
caption(head, "Pottery — one formula per dot", (640, 60), 30);
caption(sub, "vases grown bottom-up, sprigged, then re-glazed live — all pure in (i, t)", (640, 112), 20);
hidden(head);
hidden(sub);
show(head);
wait(2.4);
show(sub);
wait(11); // pots grow, sprigs sprout, then the glaze cycles the palette
cloud-kaleidoscope
A 14-fold kaleidoscope mandala in ONE cloud. The @yuruyurau original uses canvas feedback
(get()+rotate+image() — a raster trick manic doesn’t have); the OUTCOME is 14-fold rotational
symmetry, which cloud gets by layer-replication (one base field copied at 14 angles). Same picture,
but deterministic — it scrubs and records. Hue per sector, on a 9:16 Short.
// cloud-kaleidoscope — a 14-fold kaleidoscope in ONE `cloud`. The @yuruyurau
// original uses canvas FEEDBACK (get() + rotate + image()) — a raster/Droste
// trick manic doesn't have (it's vector & deterministic). But the OUTCOME is a
// 14-fold rotational symmetry, which `cloud` gets by layer-replication: one base
// field, copied at 14 angles via a layer index `L = floor(i/bn)` and rotated by
// `L·π/7`. So the picture is the same, but it scrubs and records (the p5 can't).
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated take.
//
// manic examples/cloud-kaleidoscope.manic
title("A kaleidoscope from one formula");
canvas("9:16");
template("black");
cloud(kaleido, 56000, #ffffff, 0.6) {
let bn = 4000; // points per copy (14 copies = 56k, smooth)
let L = floor(i / bn); // copy 0..13
let j = mod(i, bn); // base index
let k = mod(j, 50) - 25;
let e = j/222;
let d = 5*cos(hypot(k, e) - t + mod(j, 2));
let bx = k + k*d/6*sin(d + e/3 + t);
let by = 90 + e*d - e/d*2*cos(d + t);
let ang = L * pi/7; // 14-fold rotation of the base field
let grow = tanh(t*0.5 + 0.12);
let x = 540 + (bx*cos(ang) - by*sin(ang)) * 2.2 * grow;
let y = 960 + (bx*sin(ang) + by*cos(ang)) * 2.2 * grow;
let r = 0.8;
let hue = mod(L*26 + by*2 + t*12, 360); // a colour per sector + radial
}
// ---- textbook annotations ----
caption(head, "A kaleidoscope from one formula", (540, 138), 34);
caption(sub, "one field, copied at 14 angles", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `\vec p_L = R\!\left(L\tfrac{2\pi}{14}\right)\,\vec p_0`, 34);
caption(lab, "14-fold symmetry, no mirrors — pure rotation", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-medusae
Another @yuruyurau art-tweet in ONE cloud: 30,000 points in interleaved layers (mod(i,5/4/2))
swirl into a ring of tailed medusae. A polar plot; two JS bitwise-isms translated (**4->^4,
~(i&1)*80->(1+mod(i,2))*80). Hue-coloured, bloomed, 9:16.
// cloud-medusae — another @yuruyurau art-tweet in ONE `cloud`: 30,000 points in
// interleaved layers (`mod(i,5)`, `mod(i,4)`, `mod(i,2)`) swirl into a ring of
// tailed medusae that pulse over time. A polar plot (radius `q`, angle `c`).
// Two JS bitwise-isms translated: `**4` → `^4`, and `~(i&1)*80` (bitwise NOT of
// i&1) → `(1+mod(i,2))*80` — an 80/160 offset per parity. Hue'd + bloomed, 9:16.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our own annotated take —
// pure in (i, t), so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-medusae.manic
title("A ring of medusae from one formula");
canvas("9:16");
template("black");
cloud(medusae, 30000, #ffffff, 0.55) {
let s = i/799;
let k = 5*cos(i/48);
let e = 5*cos(s/9);
let d = (hypot(k, e)/(6 + mod(i,4)))^4 + 4;
let q = k*(3 + e/2*sin(d*8 + k/9 - t)) - 3*sin(k*d/3) + (1 + mod(i,2))*80;
let c = d - t/9 + mod(i,5);
let px = q*sin(c);
let py = q*cos(c - mod(i,2) + mod(i,5)*3 + 7);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 1.7 * grow;
let y = 960 + py * 1.7 * grow;
let hue = mod(mod(i,5)*72 + i*0.01 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "A ring of medusae from one formula", (540, 138), 32);
caption(sub, "30,000 points, no simulation", (540, 204), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; q\cos c')`, 32);
caption(lab, "a polar plot in interleaved layers (mod i,n)", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-koi
Another @yuruyurau art-tweet in ONE cloud: 10,000 points in two layers (mod(i,2)) swirl into
koi-like forms chasing each other. A polar plot with raw-index texture (cos(i+t/4)); the p5
y^9 (bitwise XOR — no manic operator) is approximated with noise(). Hue-coloured, bloomed, 9:16.
// cloud-koi — another @yuruyurau art-tweet in ONE `cloud`: 10,000 points in two
// layers (`mod(i,2)`) swirl into koi-like forms chasing each other, rippling over
// time. A polar plot (radius `q`, angle `c`); `cos(i+t/4)` on the raw index gives
// the fine scale texture. One JS-ism: the original's `y^9` is bitwise XOR (a
// per-band scramble), which manic has no operator for — approximated here with
// `noise()`, visually equivalent. `mag` = `hypot`; parameter renamed `s`.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated
// take — pure in (i, t), so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-koi.manic
title("Two koi from one formula");
canvas("9:16");
template("black");
cloud(koi, 10000, #ffffff, 0.6) {
let s = i/790;
let m = mod(i, 2);
let sw = 0.5*(1 + sign(8 - s)); // (y<8 ? … : …)
let scr = noise(s*2, 0); // ~ the JS y^9 XOR scramble
let kbase = sw*(9 + scr*6) + (1 - sw)*(4 + cos(s));
let k = kbase * cos(i + t/4);
let e = s/3 - 13;
let d = hypot(k, e) + cos(e + t*2 + m*4);
let q = s*k/5*(2 + sin(d*2 + s - t*4)) + 80;
let c = d/4 - t/2 + m*3;
let px = q*cos(c);
let py = q*sin(c) + d*9 - 130; // recentre the d*9+60 offset
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.1 * grow;
let y = 960 + py * 2.1 * grow;
let hue = mod(m*90 + i*0.03 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "Two koi from one formula", (540, 138), 36);
caption(sub, "10,000 points, no simulation", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\cos c,\;\; q\sin c)`, 32);
caption(lab, "a polar plot: radius q, angle c, per point", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
glsl-medusae-glow
Two ‘one formula’ pieces sharing a frame: a raw glsl glow field draws bioluminescent medusae as
pure inverse-square LIGHT traced along an epicycle (no geometry — every pixel just sums its distance
to the glowing thread), with an HDR tone-map so the cores bloom instead of clipping; above it, the
koi cloud swims. A particle system and a per-pixel field on one stage, both pure in (i, t).
// glsl-medusae-glow — four bioluminescent medusae drawn as pure LIGHT. Each is a
// two-frequency (epicyclic) orbit; we trace 55 time-lagged beads along it and add
// inverse-square glow, so the filament reads as a bright creature trailing into the
// dark. No geometry, no particles — every pixel just asks "how close am I to each
// glowing thread?" and sums the light. Runs on manic's raw glsl() path (Shadertoy
// mainImage, iTime/iResolution) — pure in (pixel, time), so it scrubs and records.
//
// Our take on the classic compact glow-medusae shader: the epicycle + inverse-square
// core is kept exactly; the elevation is honest post — a faint deep-sea backing and a
// tone-map so the cores bloom softly instead of clipping to flat white.
//
// manic examples/glsl-medusae-glow.manic
title("Koi through a field of living light");
canvas("16:9");
template("black");
glsl(field, `
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 r = iResolution.xy;
float t = iTime;
// aspect-correct coords, centre pushed to the LOWER third so the koi above
// has clear space (y runs about -0.66 .. +0.34 top to bottom here)
vec2 u = (fragCoord - vec2(0.5, 0.34) * r) / r.y;
vec3 col = vec3(0.0);
// four medusae — each its own hue, speed, and inner/outer frequency ratio
for (float j = 0.0; j < 4.0; j++) {
float s = 1.0 + j; // base angular speed
float k = 2.0 + j; // small-loop frequency ratio
vec3 h = 0.5 + 0.5 * cos(j * 9.0 + vec3(1.0, 2.0, 3.0)); // per-medusa hue
// nearest squared distance from this pixel to the glowing filament
float d = 1e9;
for (float i = 0.0; i < 55.0; i++) {
float e = t * 0.5 - i * 0.03; // time-lagged phase down the trail
vec2 b = vec2(cos(e * s), sin(e * s)) * 0.10 // large epicycle
+ vec2(cos(e * s * k), sin(e * s * k)) * 0.04; // small epicycle
d = min(d, dot(u - b, u - b));
}
// inverse-square light: a bright thread with a soft, wide halo
col += h * 3e-5 / (d + 5e-9);
}
// --- our elevation: honest post, same field underneath ---
col += vec3(0.010, 0.028, 0.060) * (1.0 - length(u) * 0.6); // deep-sea backing
col = 1.0 - exp(-col * 1.2); // HDR bloom, cores don't clip
col = pow(col, vec3(0.85)); // gentle gamma lift
fragColor = vec4(col, 1.0);
}
`);
// ---- the koi: 10,000 points in two layers (mod i,2) swirl into koi chasing each
// other, from cloud-koi. A polar plot (radius q, angle c). Same formula as the
// standalone piece — only recentred + rescaled for this 16:9 stage — so the fish
// swim THROUGH the glow field above. Pure in (i, t); it scrubs and records.
cloud(koi, 10000, #ffffff, 0.6) {
let s = i/790;
let m = 0; // one koi is enough (was mod i,2 = two)
let sw = 0.5*(1 + sign(8 - s)); // (y<8 ? … : …)
let scr = noise(s*2, 0); // ~ the JS y^9 XOR scramble
let kbase = sw*(9 + scr*6) + (1 - sw)*(4 + cos(s));
let k = kbase * cos(i + t/4);
let e = s/3 - 13;
let d = hypot(k, e) + cos(e + t*2 + m*4);
let q = s*k/5*(2 + sin(d*2 + s - t*4)) + 80;
let c = d/4 - t/2 + m*3;
let px = q*cos(c);
let py = q*sin(c) + d*9 - 130; // recentre the d*9+60 offset
let grow = tanh(t*0.5 + 0.12);
let x = 640 + px * 1.3 * grow; // 16:9 stage: koi in the UPPER half…
let y = 285 + py * 1.3 * grow; // …with the glow core sitting clear in the lower third
let hue = mod(m*90 + i*0.03 + t*14, 360);
}
// ---- textbook annotations (kept off the glow, backed for legibility) ----
caption(head, "Koi through a field of living light", (640, 66), 30);
plate(head);
caption(sub, "two formulas, no simulation — a glow field and a koi cloud", (640, 112), 20);
plate(sub);
hidden(head);
hidden(sub);
equation(eq, (640, 636), `b(e)=0.1\,(\cos es,\sin es)+0.04\,(\cos esk,\sin esk)`, 26);
plate(eq);
caption(lab, "medusae: light traced along an epicycle · koi: 10,000 polar points", (640, 682), 18);
plate(lab);
hidden(eq);
hidden(lab);
// let the field breathe, then bring the story in over it
wait(2.0);
show(head);
wait(1.6);
show(sub);
wait(3.0);
show(eq);
show(lab);
wait(18);
glsl-fractal-nebula
A twigl-style 3D fractal fold as a soft gold dawn with blue frost-ferns (raw glsl, accumulated
into a LOCAL vec3 — the o.rgb += in-loop idiom miscompiles on Metal), and a murmuration cloud
sweeping across it: a per-pixel field and a particle system sharing one frame, both pure in (i, t).
// glsl-fractal-nebula — a twigl-style 3D fractal fold as a soft gold dawn field
// with blue frost-ferns (raw `glsl`, accumulated into a LOCAL vec3 — the `o.rgb +=`
// in-loop idiom miscompiles on the Metal backend), and a murmuration `cloud`
// sweeping across it: a per-pixel field and a particle system sharing one frame.
// Both pure in (pixel/i, t), so the whole scene scrubs and records.
//
// manic examples/glsl-fractal-nebula.manic
title("A fractal dawn, and a murmuration");
canvas("16:9");
template("black");
glsl(scene, `
mat3 rotate3D(float angle, vec3 axis){
axis = normalize(axis);
float s = sin(angle), c = cos(angle), oc = 1.0 - c;
return mat3(
oc*axis.x*axis.x + c, oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s,
oc*axis.x*axis.y + axis.z*s, oc*axis.y*axis.y + c, oc*axis.y*axis.z - axis.x*s,
oc*axis.z*axis.x - axis.y*s, oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c
);
}
vec3 hsv(float h, float s, float v){
vec3 rgb = clamp(abs(mod(h*6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
return v * mix(vec3(1.0), rgb, s);
}
void mainImage(out vec4 o, in vec2 FC){
vec2 r = iResolution.xy;
float t = iTime;
vec3 col = vec3(0.0); // accumulate here, not into o
float i = 0., g = 0., e = 0., s = 0.;
for(int n = 0; n < 98; n++){
i += 1.0;
vec3 p = vec3((FC.xy-.5*r)/r.y*5. + vec2(0,9), g)
* rotate3D(-1.1 - cos(t*.15)*.1, vec3(1, 11.+sin(t)*.15, -1.5));
s = 2.;
for(int j = 0; j < 19; j++){
s *= e = 7.1/dot(p, p*.51);
p = vec3(.08,4,-1) - abs(abs(p)*e - vec3(3,4,3));
}
g += p.y/s;
s = log2(s)/exp(e);
col += .01 - hsv(.1, g*.016 - e*.3, s/2e2); // original's o.rgb += …, into the local
}
o = vec4(col, 1.0);
}
`);
// a murmuration sweeping across the still dawn field — a cohesive blob of birds
// (golden-angle scatter, denser core) whose centre sweeps a path, stretched along
// motion and banked into each turn, breathing organically. Pure in (i, t).
cloud(flock, 9000, #e8f6ff, 0.8) {
let s = i/9000; // 0..1 through the flock
let ang = i*2.39996; // golden-angle scatter
let rad = sqrt(s); // wispy toward the edge
let sw = ang + rad*3*sin(t*0.5) + t*0.6; // the interior swirls (shape-shifting)
let taper = 1 - 0.45*s; // tail thins out
let ex = rad*cos(sw)*235*taper; // elongated along motion…
let ey = rad*sin(sw)*88*taper; // …narrower across
let turb = 70*rad*rad; // tendrils: turbulence grows at the edge
let bx = ex + turb*sin(i*0.7 + t*2.2);
let by = ey + turb*cos(i*0.9 + t*1.9);
let phase = t*0.45;
let cx = 640 + 330*sin(phase); // the flock sweeps left↔right…
let cy = 250 + 80*sin(phase*1.6 + 0.7); // …rising and dipping
let bank = 0.6*cos(phase); // and banks into each turn
let rx = bx*cos(bank) - by*sin(bank);
let ry = bx*sin(bank) + by*cos(bank);
let grow = tanh(t*0.6 + 0.1);
let x = cx + rx*grow;
let y = cy + ry*grow;
let hue = mod(210 + s*14 + t*5, 360);
}
wait(12);
cloud-feathers
Another @yuruyurau art-tweet in ONE cloud: 20,000 points in FOUR layers (mod(i,4)) fan into
feathery plumes that flutter — a polar plot whose amplitude switches on a conditional (the p5
ternary becomes a sign() blend, since cloud formulas have none). Hue-coloured, bloomed, 9:16.
// cloud-feathers — another @yuruyurau art-tweet in ONE `cloud`: 20,000 points in
// FOUR layers (`mod(i,4)`) fan into feathery plumes that flutter over time. A
// polar plot (radius `q`, angle `c`) whose amplitude switches on a conditional —
// the original's `(y<5 ? … : 11)` becomes a `sign()` blend, since cloud formulas
// have no ternary. `mag(k,e)` here (not squared); coloured per point and bloomed
// on a 9:16 Short. The parameter is renamed `s` (the required output is `y`).
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated
// take — pure in (i, t), so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-feathers.manic
title("Four feathers from one formula");
canvas("9:16");
template("black");
cloud(feathers, 20000, #ffffff, 0.6) {
let s = i/500; // reference's "y"
let m = mod(i, 4) * 8; // four layers
let sw = 0.5*(1 + sign(5 - s)); // (y<5 ? … : …) via sign
let kamp = sw*(sin(t/8 + s)*35) + (1 - sw)*11;
let k = cos(s*9) * kamp;
let e = s/8 - 13;
let o = hypot(k, e)/6;
let q = k*s/19 + 49 + k*sin(s)*sin(o*2 - e/5 - t);
let c = o/3 - e/5 - t/8 + m;
let px = q*sin(c) - 79*cos(c/3);
let py = (q + 70)*cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 1.9 * grow;
let y = 960 + py * 1.9 * grow;
let hue = mod(m*45 + i*0.04 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "Four feathers from one formula", (540, 138), 34);
caption(sub, "20,000 points, no simulation", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q{+}70)\cos c)`, 30);
caption(lab, "a polar plot in four layers (mod i,4)", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-jellyfish
Another @yuruyurau art-tweet in ONE cloud: 20,000 points in two layers (mod(i,2)) drift into
jellyfish-like bells with trailing tendrils and pulse over time — a polar plot with a nested
sin(sin(...)) ripple, hue-coloured and bloomed on a 9:16 Short. Pure in (i, t), so it scrubs.
// cloud-jellyfish — another @yuruyurau art-tweet in ONE `cloud`: 20,000 points in
// two layers (`mod(i,2)`) drift into jellyfish-like bells with trailing tendrils
// and pulse over time. A polar plot (radius `q`, angle `c`) with a nested
// `sin(sin(...))` that gives the bell its soft ripple; coloured per point and
// bloomed from the centre on a 9:16 Short. `mag(k,e)^2` → `k*k+e*e`; the
// parameter is renamed `s` (the required output is `y`).
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated
// take — pure in (i, t), so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-jellyfish.manic
title("Two jellyfish from one formula");
canvas("9:16");
template("black");
cloud(jelly, 20000, #ffffff, 0.6) {
let s = i/99; // reference's "y" parameter
let m = mod(i, 2) * 3; // two layers
let k = 9*cos(s*2);
let e = s/8 - 12;
let d = (k*k + e*e)/79 + 1;
let q = 79 - e*sin(k) + k/d*(8 + 4*sin(sin(d*d + e/9 - t)));
let c = d/2 - cos(d*2)/5 - t/16 + m;
let px = q*sin(c);
let py = (q + 40)*cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.2 * grow;
let y = 960 + py * 2.2 * grow;
let hue = mod(m*70 + i*0.05 + t*15, 360);
}
// ---- textbook annotations ----
caption(head, "Two jellyfish from one formula", (540, 138), 34);
caption(sub, "20,000 points, no simulation", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q{+}40)\cos c)`, 30);
caption(lab, "a polar plot: radius q, angle c, per point", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-jellyfish-v2
The jellyfish cloud, now SWIMMING IN A SHADER AQUARIUM — a demo that a particle system and a
per-pixel field share one frame. The bells are the v1 cloud, re-lit into a cyan↔magenta
bioluminescent band; everything around them is one shader: a depth gradient (teal surface →
deep navy), animated caustics, and soft god-rays from the surface. Rising bubbles are a second
tiny cloud. Two clouds + one ocean shader + captions, all pure in (i, t) — the whole tank scrubs.
// cloud-jellyfish-v2 — the @yuruyurau jellyfish `cloud` (20,000 points, two layers,
// pulsing bells + tendrils), now SWIMMING IN A SHADER AQUARIUM. The bells are the same
// particle system as v1; everything around them is one per-pixel `shader`: a depth
// gradient (teal surface → deep navy), animated caustics rippling near the top, and soft
// god-rays falling from the surface. A third element — rising bubbles — is a second tiny
// `cloud`. Particle art + a per-pixel ocean + generic captions, all in one 9:16 frame,
// all pure in (i, t) so the whole aquarium scrubs and records exactly.
//
// manic examples/cloud-jellyfish-v2.manic
title("Jellyfish in a shader aquarium");
canvas("9:16");
template("black");
// ===================== the aquarium — one shader, per pixel =====================
shader(water) {
let x = (u - 0.5) * asp;
let y = v; // 0 = surface (top), 1 = deep (bottom)
let depth = smoothstep(0.0, 1.0, y);
// deep-water colour ramp: bright teal near the surface, deep blue below
let hue = mix(186, 216, depth);
let base = mix(0.26, 0.045, depth);
// caustics — warped interference, bright veins that fade with depth
let wx = x * 4.0 + 0.5 * sin(y * 6.0 + t * 0.4);
let wy = y * 7.0 + 0.5 * sin(x * 5.0 - t * 0.5);
let cc = sin(wx + t * 0.7) + sin(wy - t * 0.6) + sin((wx + wy) * 0.7 + t * 0.5);
let b = 0.5 + 0.5 * sin(cc * 1.5);
let caust = b * b * b * (1.0 - depth * 0.75);
// god-rays — soft vertical light shafts from the surface, strongest up top
let ray = 0.5 + 0.5 * sin(x * 3.0 + 0.6 * sin(t * 0.2));
let r2 = ray * ray;
let rays = r2 * r2 * (1.0 - smoothstep(0.0, 0.65, y)) * 0.45;
let val = clamp(base + caust * 0.5 + rays, 0.0, 0.95);
let sat = mix(0.85, 0.62, caust); // bright veins read a touch whiter
}
// ===================== the jellyfish — the v1 cloud, re-lit ====================
// same polar formula as v1; the hue is pulled into a cyan↔magenta bioluminescent band
// so the bells glow like sea creatures against the water instead of full-spectrum.
cloud(jelly, 20000, #ffffff, 0.6) {
let s = i/99; // reference's "y" parameter
let m = mod(i, 2) * 3; // two layers
let k = 9*cos(s*2);
let e = s/8 - 12;
let d = (k*k + e*e)/79 + 1;
let q = 79 - e*sin(k) + k/d*(8 + 4*sin(sin(d*d + e/9 - t)));
let c = d/2 - cos(d*2)/5 - t/16 + m;
let px = q*sin(c);
let py = (q + 40)*cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.2 * grow;
let y = 960 + py * 2.2 * grow;
let hue = mod(198 + m*30 + 46*sin(s*0.18 + t*0.35), 360); // cyan ↔ magenta glow
}
// ---- rising bubbles — a second tiny cloud drifting up the tank ----
cloud(bubbles, 130, #dff4ff, 0.5) {
let sp = 0.05 + 0.06 * rand2(i, 1.3); // per-bubble rise speed
let ph = rand2(i, 2.7);
let prog = fract(ph + t * sp); // 0 → 1 rise progress
let x = 1080 * rand2(i, 4.1) + 24 * sin(prog * tau * 2.0 + i);
let y = 1920 * (1.0 - prog); // bottom → top
let r = 2.0 + 5.0 * rand2(i, 5.5);
let hue = 196;
let alpha = 0.5 * sin(prog * pi); // fade in low, fade out near the surface
}
// ---- textbook annotations ----
caption(head, "Jellyfish in a shader aquarium", (540, 138), 33);
caption(sub, "a 20,000-point cloud + a per-pixel ocean", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q{+}40)\cos c)`, 30);
caption(lab, "bells: a polar cloud · water: one shader · bubbles: a second cloud", (540, 1786), 19);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-siphonophore
Another @yuruyurau art-tweet in ONE cloud, reimagined as a bioluminescent deep-sea drifter:
12,000 points trace a morphing bell + long feathered tail (polar radius q, angle c, with a
d^sin(2d-t/3) body-warp) that swims and reconfigures over time. Re-lit in the abyss — additive
glow + bloom make it luminous, the hue drifts cyan bell → violet tail, and faint marine snow
sifts down past it. Pure in (i, t), so it scrubs; the p5 original can’t.
// cloud-siphonophore — another @yuruyurau creature in ONE `cloud`, reimagined as a
// bioluminescent deep-sea drifter. The reference is a tweet-sized golf:
// k = 5cos(i/14)cos(y/30), e = y/8-13, d = (k²+e²)/59 + 6,
// q = 90 - 5sin(atan2(k,e)·e) + k(3+sin(d²-2t)), c = d/2 - t/18,
// point( q·sin c , (q + d·d^sin(2d-t/3))·cos c )
// A morphing bell trailing a long feathered tail (10,000 points, pure in i,t). Here it
// glows in the abyss: additive `glow` + `bloom` make it luminous, the hue drifts along
// the body (cyan bell → violet tail), and faint marine snow sifts down past it.
//
// manic examples/cloud-siphonophore.manic
title("Siphonophore — a yuruyurau creature in the abyss");
canvas("9:16");
template("black");
bloom(0.42, 0.5, 34);
// ---- the abyss: a near-black depth gradient, faint cold light from above ----
shader(abyss) {
let y = v;
let depth = smoothstep(0.0, 1.0, y);
let hue = mix(206, 244, depth);
let val = mix(0.05, 0.006, depth) + 0.02 * (1.0 - smoothstep(0.0, 0.5, y));
let sat = 0.8;
}
z(abyss, -10);
// ---- the creature — the yuruyurau golf, re-lit ----
cloud(crea, 12000, #ffffff, 0.30) {
let yy = i / 43.0;
let k = 5.0 * cos(i / 14.0) * cos(yy / 30.0);
let e = yy / 8.0 - 13.0;
let d = (k*k + e*e) / 59.0 + 6.0;
let T = t * 3.0;
let q = 90.0 - 5.0*sin(atan2(k, e) * e) + k*(3.0 + sin(d*d - T*2.0));
let c = d/2.0 - T/18.0;
let px = q * sin(c);
let py = (q + d * d^sin(d*2.0 - T/3.0)) * cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 3.5 * grow;
let y = 960 + py * 3.5 * grow;
// bioluminescence: cyan bell (small d) → violet tail (large d), shimmering
let hue = mod(184.0 + d * 15.0 + 26.0*sin(yy*0.08 + t*0.4), 360);
let sat = 0.85;
let val = clamp(0.55 + 0.45*sin(d*d - T*2.0), 0.22, 1.0);
let r = 1.35;
}
glow(crea, 1);
// ---- marine snow — a second tiny cloud sifting down past the creature ----
cloud(snow, 150, #dfeeff, 0.4) {
let sp = 0.02 + 0.03 * rand2(i, 1.3);
let ph = rand2(i, 2.7);
let prog = fract(ph + t * sp);
let x = 1080 * rand2(i, 4.1) + 16 * sin(prog * tau + i);
let y = 1920 * prog;
let r = 1.4 + 3.0 * rand2(i, 5.5);
let hue = 200;
let alpha = 0.4 * sin(prog * pi);
}
// ---- annotations ----
caption(head, "Siphonophore in the abyss", (540, 140), 32); hidden(head);
caption(sub, "a 12,000-point cloud, glowing", (540, 206), 22); hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q + d\,d^{\sin(2d-t/3)})\cos c)`, 26); hidden(eq);
caption(lab, "one @yuruyurau golf · additive glow · bloom", (540, 1786), 19); hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(24);
probability-as-area
The two models 3Blue1Brown built the unreleased ‘Essence of Probability’ on, both area arguments rather than formulas. ACT I the BRICK ROW: one brick of probability 1, and every coin flip splits each brick in two - same-outcome bricks merge, so the widths become C(n,k)/2^n and the binomial EMERGES from halving area (1:4:6:4:1 by level four). ACT II the AREA MODEL: a unit square cut across by the prior and down by the test’s accuracy, so Bayes is one bright rectangle over two - a positive test means two chances in three, not ninety percent. No probability builtins were needed: binomial widths are build-time prod/sum reductions and the splits are to(id, width).
// probability-as-area — the two models 3Blue1Brown built the (unreleased) "Essence of
// Probability" series on, both of which are area arguments rather than formulas.
//
// ACT I the BRICK ROW (`_2018/eop/reusables/brick_row.py`): one brick of probability
// 1, and every coin flip splits each brick in two. Bricks with the same number
// of heads merge, so the widths become C(n,k)/2^n and the binomial distribution
// EMERGES from repeatedly halving area — 1 : 4 : 6 : 4 : 1 by level four.
// ACT II the AREA MODEL (`_2018/eop/chapter1/area_model_bayes.py`): a unit square cut
// by the prior across and the test's accuracy down, so Bayes' theorem is just
// one bright rectangle over two.
//
// Written with ordinary manic vocabulary — the binomial coefficients are build-time
// `prod`/`sum` reductions over the loop index, and the splits are `to(id, width, …)` /
// `to(id, height, …)`. Growth is about a rect's centre, so each one is paired with
// `to(id, x, …)` / `to(id, y, …)` in a `par` to anchor the edge that should stay put.
//
// manic examples/probability-as-area.manic
title("Probability is area you keep splitting");
canvas("16:9");
template("black");
bloom(0.3, 0.5, 22);
text(head, (640, 76), "Probability is area you keep splitting");
display(head); size(head, 36); bold(head); color(head, fg); hidden(head);
text(cap, (640, 660), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
// ---- ACT I — the brick row ----
let tw = 880; // the row is one unit of probability, 880px wide
let x0 = 200;
for k in 0..5 {
rect(b{k}, (x0, 330), 0, 104);
hue(b{k}, 188 + 26*k);
}
rect(frame, (x0 + tw/2, 330), tw, 104); outlined(frame); outline(frame, dim); hidden(frame);
text(counts, (640, 262), ""); display(counts); size(counts, 26); color(counts, gold); hidden(counts);
text(flips, (640, 420), ""); display(flips); size(flips, 21); color(flips, dim); hidden(flips);
// ---- ACT II — the area model ----
// a unit square: the prior runs ACROSS (10% of people are ill), the test's accuracy runs
// DOWN (it catches 90% of the ill, and wrongly flags 5% of the well)
let sq = 396;
let sqx = 640; // left edge — the diagram sits right, the maths left
let sqy = 168; // top edge
rect(ill, (sqx, sqy), 0, 0); hue(ill, 320); // ill AND positive
rect(illn, (sqx, sqy), 0, 0); hue(illn, 300); // ill AND negative
rect(wellp, (sqx, sqy), 0, 0); hue(wellp, 196); // well AND positive (false alarm)
rect(well, (sqx + sq/2, sqy + sq/2), sq, sq); hue(well, 208); // everyone, to begin with
hidden(ill); hidden(illn); hidden(wellp); hidden(well);
rect(border, (sqx + sq/2, sqy + sq/2), sq, sq); outlined(border); outline(border, dim); hidden(border);
text(xlab, (sqx + sq/2, sqy + sq + 34), ""); display(xlab); size(xlab, 19); color(xlab, dim); hidden(xlab);
text(ylab, (sqx - 74, sqy + sq/2), ""); display(ylab); size(ylab, 19); color(ylab, dim); hidden(ylab);
equation(bayes, (300, 366), `P(\text{ill}\mid+)=\frac{0.09}{0.09+0.045}=\tfrac{2}{3}`, 27);
color(bayes, gold); hidden(bayes);
// ============================ ACT I ============================
show(head, 0.7);
show(cap, 0.3);
say(cap, "Start with one brick. Its width is the whole of probability: one.");
show(frame, 0.5);
par { to(b0, width, tw, 0.6, smooth); to(b0, x, x0 + tw/2, 0.6, smooth); }
say(counts, "1");
wait(1.2);
say(cap, "A coin flip splits every brick in two — heads to the left, tails to the right.");
show(flips, 0.4);
// each flip: the widths become C(L,k)/2^L. The product is naturally zero for k > L, so a
// brick that does not exist yet simply has no width.
for L in 1..5 {
par {
for k in 0..5 {
to(b{k}, width, tw * prod(j in 1..k+1 : (L - k + j)/j) / 2^L, 0.85, smooth);
to(b{k}, x,
x0 + tw*sum(m in 0..k : prod(j in 1..m+1 : (L - m + j)/j))/2^L
+ tw*prod(j in 1..k+1 : (L - k + j)/j)/(2*2^L),
0.85, smooth);
}
}
wait(0.75);
}
show(counts, 0.4);
say(counts, "1 4 6 4 1");
say(flips, "four flips · widths are C(4,k) / 16");
wait(1.0);
say(cap, "Bricks with the same number of heads merge, and the binomial falls out of the area.");
wait(2.4);
// ============================ ACT II ============================
par {
fade(counts, 0.5);
fade(flips, 0.5);
fade(frame, 0.5);
for k in 0..5 {
fade(b{k}, 0.6);
}
}
say(cap, "The same trick answers a harder question. One square: everybody.");
show(well, 0.5);
show(border, 0.5);
wait(1.4);
say(cap, "Cut it ACROSS by how common the illness is — one person in ten.");
show(xlab, 0.4);
say(xlab, "10% ill · 90% well");
show(ill, 0.01);
par {
// the well column keeps its right edge; the ill column takes the left tenth
to(well, width, 0.9*sq, 0.9, smooth);
to(well, x, sqx + 0.55*sq, 0.9, smooth);
to(ill, width, 0.1*sq, 0.9, smooth);
to(ill, x, sqx + 0.05*sq, 0.9, smooth);
to(ill, height, sq, 0.9, smooth);
to(ill, y, sqy + sq/2, 0.9, smooth);
}
wait(1.6);
say(cap, "Now cut it DOWN by what the test does: it catches 90% of the ill —");
show(ylab, 0.4);
say(ylab, "test + / −");
show(illn, 0.01);
par {
to(ill, height, 0.9*sq, 0.9, smooth);
to(ill, y, sqy + 0.45*sq, 0.9, smooth);
to(illn, width, 0.1*sq, 0.9, smooth);
to(illn, x, sqx + 0.05*sq, 0.9, smooth);
to(illn, height, 0.1*sq, 0.9, smooth);
to(illn, y, sqy + 0.95*sq, 0.9, smooth);
}
wait(1.4);
say(cap, "— and wrongly flags 5% of the well. That thin strip is the false alarms.");
show(wellp, 0.01);
par {
to(well, height, 0.95*sq, 0.9, smooth);
to(well, y, sqy + 0.525*sq, 0.9, smooth);
to(wellp, width, 0.9*sq, 0.9, smooth);
to(wellp, x, sqx + 0.55*sq, 0.9, smooth);
to(wellp, height, 0.05*sq, 0.9, smooth);
to(wellp, y, sqy + 0.025*sq, 0.9, smooth);
}
wait(1.8);
say(cap, "A positive test means you are in one of the two bright rectangles. Which one is bigger?");
par {
fade(illn, 0.6);
fade(well, 0.6);
}
par {
pulse(ill, 0.8);
pulse(wellp, 0.8);
}
wait(1.6);
say(cap, "The false alarms are only half the size — so a positive means two chances in three, not ninety percent.");
show(bayes, 0.8);
wait(3.0);
// ============================ ENDCARD ============================
par {
fade(ill, 0.8);
fade(wellp, 0.8);
fade(border, 0.6);
fade(xlab, 0.5);
fade(ylab, 0.5);
fade(bayes, 0.8);
fade(cap, 0.6);
fade(head, 0.6);
}
text(end1, (640, 348), "Two models, one idea: keep the area, cut it up.");
display(end1); size(end1, 40); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
multivariable-chain-rule
3Blue1Brown’s _2018/mvcr.py, whose single scene is called ComputationalNetwork for a reason:
the multivariable chain rule taught as a GRAPH, and the graph is the one backpropagation runs on.
x feeds f = x^2 and g = cos(pi x); both feed h = f^2 g, so x reaches h along TWO paths. Build the
network box by box, feed 2 forward (f=4, g=1, h=16), then nudge x and watch h swing 32 times as
far - the h dot is a one-point cloud reading the scene parameter, the only way to place a dot
where a NONLINEAR function of a live value says it goes. Then one derivative per EDGE, one product
per PATH, summed: mathparts makes each term its own entity so a framebox can surround them in
turn, and rewrite carries the equation from symbolic to substituted to 32 + 0 = 32. It ends by
running the same rule backwards through a real network - which is what backpropagation is.
// multivariable-chain-rule — 3Blue1Brown's `_2018/mvcr.py`, whose single scene is called
// `ComputationalNetwork` for a reason: the multivariable chain rule is taught as a GRAPH,
// and the graph is the one backpropagation runs on.
//
// x ──→ f = x² ──→ h = f²g
// └──→ g = cos(πx) ──┘
//
// ACT I build the network, box by box, edge by edge
// ACT II feed one number forward: x=2 → f=4, g=1 → h=16
// ACT III the question — nudge x, and h swings THIRTY-TWO times as far
// ACT IV one derivative per EDGE, and one term per PATH from x to h
// ACT V assemble, substitute, evaluate: 32 + 0 = 32
// ACT VI the same rule with a thousand nodes has another name
//
// Written with ordinary vocabulary: `mathparts` makes each term of the rule its own entity
// so a `framebox` can `surround` them in turn, `rewrite` carries one equation through its
// symbolic → substituted → evaluated states, and the sensitivity beat is a one-point
// `cloud` reading the scene `parameter` — the only way to put a dot where a NONLINEAR
// function of a live value says it goes.
//
// manic examples/multivariable-chain-rule.manic
title("The Multivariable Chain Rule — One Term Per Path");
canvas("16:9");
template("black");
bloom(0.28, 0.55, 20);
// ---- type ----
text(ttl, (640, 92), "The Multivariable Chain Rule");
display(ttl); size(ttl, 46); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 146), "one term per path through the graph");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 664), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1060, 620), ""); display(act); size(act, 19); color(act, gold); hidden(act);
// ================================ THE NETWORK ================================
// four boxed formulas. The colour of a letter is the colour of its node, everywhere it
// appears — that is the whole reading aid, and LaTeX does it inline.
equation(ex, (196, 300), `\textcolor{gold}{x}`, 34);
equation(ef, (520, 206), `\textcolor{cyan}{f} = \textcolor{gold}{x}^2`, 30);
equation(eg, (520, 396), `\textcolor{lime}{g} = \cos(\pi \textcolor{gold}{x})`, 30);
equation(eh, (912, 300), `\textcolor{magenta}{h} = \textcolor{cyan}{f}^2 \textcolor{lime}{g}`, 30);
hidden(ex); hidden(ef); hidden(eg); hidden(eh);
framebox(bx, ex, 14); color(bx, dim); untraced(bx);
framebox(bf, ef, 14); color(bf, dim); untraced(bf);
framebox(bg, eg, 14); color(bg, dim); untraced(bg);
framebox(bh, eh, 14); color(bh, dim); untraced(bh);
// the four edges, each an arrow between two boxes
arrow(axf, (250, 282), (410, 218)); color(axf, dim); untraced(axf);
arrow(axg, (250, 318), (410, 384)); color(axg, dim); untraced(axg);
arrow(afh, (636, 218), (800, 282)); color(afh, dim); untraced(afh);
arrow(agh, (636, 384), (800, 318)); color(agh, dim); untraced(agh);
tag(axf, edges); tag(axg, edges); tag(afh, edges); tag(agh, edges);
// ---- the forward pass ----
equation(vx, (196, 352), `= \textcolor{gold}{2}`, 26); color(vx, gold); hidden(vx);
equation(vf, (520, 256), `= \textcolor{cyan}{4}`, 26); color(vf, cyan); hidden(vf);
equation(vg, (520, 446), `= \textcolor{lime}{1}`, 26); color(vg, lime); hidden(vg);
equation(vh, (912, 352), `= \textcolor{magenta}{16}`, 26); color(vh, magenta); hidden(vh);
// ---- one derivative per edge ----
equation(dfx, (300, 196), `\frac{df}{dx} = 2x`, 24); color(dfx, cyan); hidden(dfx);
equation(dgx, (300, 424), `\frac{dg}{dx} = -\pi\sin(\pi x)`, 24); color(dgx, lime); hidden(dgx);
equation(dhf, (742, 196), `\frac{\partial h}{\partial f} = 2fg`, 24); color(dhf, cyan); hidden(dhf);
equation(dhg, (742, 424), `\frac{\partial h}{\partial g} = f^2`, 24); color(dhg, lime); hidden(dhg);
// ================================ THE SENSITIVITY ================================
// x lives on 0..4, h on 0..32 — the same nudge is small on one line and large on the other
parameter(xv, (150, 606), 2, 1.8, 2.2, "x", 3); hidden(xv.widget);
numberline(xline, (400, 300), 200, 0, 4, 1); color(xline, dim); hidden(xline);
numberline(hline, (900, 440), 260, 0, 32, 8); color(hline, dim); hidden(hline);
text(xtag, (400, 236), "x"); display(xtag); size(xtag, 24); color(xtag, gold); hidden(xtag);
equation(htag, (900, 372), `h = x^4\cos(\pi x)`, 26); color(htag, magenta); hidden(htag);
dot(xdot, (400, 300), 8); color(xdot, gold); hidden(xdot);
bind(xv, xdot, x, 380, 420); // 1.8 and 2.2 in the line's OWN pixels: a small nudge
// the h dot has to sit where a nonlinear function of the live parameter says: one point,
// one formula, re-evaluated every frame
cloud(hdot, 1, magenta) {
let hv = xv*xv*xv*xv*cos(pi*xv);
let x = 640 + 520*hv/32; // the line's 0 is at 640, its 32 at 1160
let y = 440;
let r = 8;
}
hidden(hdot);
// ================================ THE RULE ================================
// each part is its own entity, so a highlight box can visit them one at a time
mathparts(rule, (640, 520),
`\frac{dh}{dx} =`,
`\;\frac{df}{dx}\frac{\partial h}{\partial f}`,
`\; + \;`,
`\frac{dg}{dx}\frac{\partial h}{\partial g}`,
30);
color(rule.0, fg); color(rule.1, cyan); color(rule.2, dim); color(rule.3, lime);
hidden(rule);
framebox(mark, rule.1, 10); color(mark, gold); hidden(mark);
equation(subst, (640, 588), `= (2\cdot 2)(2\cdot 4\cdot 1) \; + \; (-\pi\sin 2\pi)(4^2)`, 28);
color(subst, fg); hidden(subst);
// ================================ THE CODA ================================
network(net, (640, 380), "3 5 4 2", "relu relu softmax", 620, 380, 7);
hidden(net);
// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.2);
show(cap, 0.3);
say(cap, "One input. Two things computed from it. One thing computed from those two.");
par { fade(ttl, 0.8); fade(sub, 0.8); }
show(ex, 0.5);
draw(bx, 0.5);
wait(0.5);
par { draw(axf, 0.6); draw(axg, 0.6); }
par { show(ef, 0.6); show(eg, 0.6); }
par { draw(bf, 0.5); draw(bg, 0.5); }
wait(0.6);
say(cap, "Both of them feed the same last box, so x reaches h along TWO different paths.");
par { draw(afh, 0.6); draw(agh, 0.6); }
show(eh, 0.6);
draw(bh, 0.5);
wait(2.0);
// ================================= ACT II =================================
say(act, "II · feed it forward");
show(act, 0.4);
say(cap, "Put in x = 2. Everything downstream follows: f is 4, g is 1, so h is 16.");
stagger(0.5) {
show(vx, 0.4);
show(vf, 0.4);
show(vg, 0.4);
show(vh, 0.4);
}
wait(1.8);
// ================================= ACT III =================================
say(act, "III · how sensitive is h?");
say(cap, "Now the only question that matters: nudge x a little — how far does h move?");
par {
to(ex, opacity, 0.25, 0.6);
to(ef, opacity, 0.25, 0.6);
to(eg, opacity, 0.25, 0.6);
to(eh, opacity, 0.25, 0.6);
to(vx, opacity, 0.2, 0.6);
to(vf, opacity, 0.2, 0.6);
to(vg, opacity, 0.2, 0.6);
to(vh, opacity, 0.2, 0.6);
to(edges, opacity, 0.2, 0.6);
to(bx, opacity, 0.15, 0.6);
to(bf, opacity, 0.15, 0.6);
to(bg, opacity, 0.15, 0.6);
to(bh, opacity, 0.15, 0.6);
}
par { show(xline, 0.5); show(hline, 0.5); }
par { show(xtag, 0.4); show(htag, 0.5); show(xdot, 0.4); show(hdot, 0.4); }
wait(0.8);
say(cap, "Watch the two dots. The same wiggle, on two very different scales.");
to(xv, value, 2.2, 1.1, smooth);
to(xv, value, 1.8, 1.6, smooth);
to(xv, value, 2, 0.9, smooth);
wait(0.6);
say(cap, "x moved a fifth of a unit. h moved six. The ratio it settles on is dh/dx = 32.");
wait(2.4);
// ================================= ACT IV =================================
say(act, "IV · one derivative per edge");
par {
fade(xline, 0.5); fade(hline, 0.5); fade(xtag, 0.4); fade(htag, 0.5);
fade(xdot, 0.4); fade(hdot, 0.4);
to(ex, opacity, 1, 0.6);
to(ef, opacity, 1, 0.6);
to(eg, opacity, 1, 0.6);
to(eh, opacity, 1, 0.6);
to(edges, opacity, 1, 0.6);
to(bx, opacity, 1, 0.6);
to(bf, opacity, 1, 0.6);
to(bg, opacity, 1, 0.6);
to(bh, opacity, 1, 0.6);
to(vx, opacity, 0.35, 0.6);
to(vf, opacity, 0.35, 0.6);
to(vg, opacity, 0.35, 0.6);
to(vh, opacity, 0.35, 0.6);
}
say(cap, "Every EDGE carries a derivative: how much its head moves when its tail moves.");
par { show(dfx, 0.5); show(dgx, 0.5); }
wait(1.4);
say(cap, "The last two are PARTIAL — hold the other input still while you wiggle this one.");
par { show(dhf, 0.5); show(dhg, 0.5); }
wait(2.2);
say(cap, "Follow the top path: x changes f, f changes h. Multiply the two.");
par { pulse(axf, 0.8); pulse(afh, 0.8); }
wait(1.6);
say(cap, "Then the bottom path: x changes g, g changes h. Multiply those too — and ADD.");
par { pulse(axg, 0.8); pulse(agh, 0.8); }
wait(2.0);
// ================================= ACT V =================================
say(act, "V · assemble it");
say(cap, "That is the whole rule: one product per path, summed over every path.");
show(rule, 0.8);
wait(1.2);
show(mark, 0.5);
say(cap, "The top path — df/dx times the partial of h in f.");
wait(1.6);
surround(mark, rule.3, 0.8, smooth);
say(cap, "The bottom path — dg/dx times the partial of h in g. Nothing else contributes.");
wait(2.0);
fade(mark, 0.5);
say(cap, "Substitute what we know at x = 2, where f = 4 and g = 1.");
show(subst, 0.8);
wait(2.2);
say(cap, "Sine of two pi is zero, so the bottom path contributes NOTHING here. 32 plus 0.");
rewrite(subst, `= 32 \; + \; 0 \;=\; \textcolor{magenta}{32}`, 1.4, smooth);
wait(2.6);
say(cap, "The same 32 the wiggling dots found — and h = x⁴cos(πx) agrees, if you expand it.");
wait(2.4);
// ================================= ACT VI =================================
say(act, "VI · at scale");
par {
fade(ex, 0.6); fade(ef, 0.6); fade(eg, 0.6); fade(eh, 0.6);
fade(bx, 0.5); fade(bf, 0.5); fade(bg, 0.5); fade(bh, 0.5);
fade(edges, 0.5);
fade(vx, 0.4); fade(vf, 0.4); fade(vg, 0.4); fade(vh, 0.4);
fade(dfx, 0.5); fade(dgx, 0.5); fade(dhf, 0.5); fade(dhg, 0.5);
fade(rule, 0.7); fade(subst, 0.7);
}
say(cap, "Four boxes and two paths. Now give the same rule a few thousand of each.");
show(net, 1.0);
forward(net, "0.9 0.2 0.6", 2.0);
wait(0.6);
loss(net, "1 0", crossentropy, 1.2);
say(cap, "Every weight is an edge, every edge carries a derivative, every path gets summed.");
backward(net, 3.0, smooth);
wait(1.6);
say(cap, "Run it backwards and the chain rule has another name: backpropagation.");
wait(2.6);
// ================================= ENDCARD =================================
par {
fade(net, 0.9);
fade(cap, 0.7);
fade(act, 0.6);
}
text(end1, (640, 340), "One product per path. Sum over paths.");
display(end1); size(end1, 42); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
sphere-area
Why a sphere’s area is 4piR^2 — 3Blue1Brown’s _2018/sphere_area.py, which is to say why a
sphere is exactly FOUR of its own shadows. ACT I the question: a sphere, the disc it shadows,
and the factor of four. ACT II Archimedes’ map — the sphere is cut into 336 tiles (pieces3)
and every tile is pushed radially outward onto the enclosing cylinder; a cross-section shows
why nothing tears (a tile at distance d lands at R, so its width scales by R/d while it leans
by d/R, and the product is 1). ACT III that cylinder unrolls into a flat 2piR x 2R rectangle.
ACT IV four discs, each unrolled ring by ring into a right triangle of base 2piR and height R,
tile that rectangle exactly. Sphere, cylinder and flat sheet are ONE param3 on one parameter
journey, and the tiles re-sample it every frame, so the whole tiling rides the map.
// sphere-area — 3Blue1Brown's `_2018/sphere_area.py`: why the area of a sphere is 4πR²,
// which is to say why it is exactly FOUR of its own shadows.
//
// ACT I the question — a sphere, its shadow, and the factor of four
// ACT II Archimedes' map — cut the sphere into tiles (`pieces3`) and push every tile
// radially outward onto the enclosing cylinder. Nothing tears: each tile gets
// WIDER by R/d and SHORTER by d/R, and R/d · d/R = 1, so area is preserved.
// ACT III unwrap that cylinder — a flat 2πR × 2R rectangle
// ACT IV and four discs, each unrolled ring by ring into a right triangle of area πR²,
// tile it exactly. Four circles. That is the whole answer.
//
// The sphere, the cylinder and the flat rectangle are ONE `param3` on one parameter journey
// (0 → sphere, 1 → cylinder, 2 → unwrapped), and the tiles are `pieces3` of it — so the
// pieces are re-sampled from the surface's own formulas every frame and travel with it.
// The four unrolling discs are `cloud`s reading the same kind of parameter. R = 1 throughout,
// so the numbers on screen are the theorem: 2π · 2 = 4π.
//
// manic examples/sphere-area.manic
title("Sphere Area — Four Circles, Wrapped");
canvas("16:9");
template("black");
bloom(0.3, 0.5, 22);
// ---- type ----
text(ttl, (640, 96), "Why 4πR²?");
display(ttl); size(ttl, 52); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 152), "a sphere is exactly four of its own shadows");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 664), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1050, 622), ""); display(act); size(act, 19); color(act, gold); hidden(act);
// ================================ THE 3-D STAGE ================================
camera3((4.6, -5.4, 3.2), (0, 0, 0), 46, perspective);
// One surface, three shapes. `map` = 0 the unit sphere, 1 the enclosing cylinder (every
// point pushed straight out from the axis, keeping its height), 2 that cylinder unwrapped
// into the flat 2π × 2 rectangle. `clamp` splits the journey into its two halves.
parameter(map, (150, 606), 0, 0, 2, "map", 2); hidden(map.widget);
param3(shell,
"(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*cos(u) + clamp(p-1,0,1)*(u-pi)",
"(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*sin(u) - clamp(p-1,0,1)*cos(v)",
"(1-clamp(p-1,0,1))*cos(v)",
(0, tau), (0.02, 3.12), 28);
bind(map, shell, formula,
"(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*cos(u) + clamp(p-1,0,1)*(u-pi)",
"(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*sin(u) - clamp(p-1,0,1)*cos(v)",
"(1-clamp(p-1,0,1))*cos(v)");
color(shell, cyan); finish3(shell, "wire=1"); hidden(shell);
// the same surface as 336 loose tiles — they re-sample the surface every frame, so the
// whole tiling rides the map out onto the cylinder and then flat
pieces3(tiles, shell, 24, 14, 0.12);
hue(tiles, 196);
hue(tiles.row7, 320); // one latitude band, to watch a single row travel
hidden(tiles);
// the shadow: the disc the sphere covers, on the ground
param3(shade, "v*cos(u)", "v*sin(u)", "-1.05", (0, tau), (0.02, 1), 24);
color(shade, gold); hidden(shade);
grid3(floor, (0, 0, -1.06), 2, 0.5); color(floor, dim); hidden(floor);
// ================================ THE 2-D STAGE ================================
// the unwrapped rectangle, in screen space. R = 110px, so it is 2piR = 691 wide and 2R = 220
// tall, and four discs of radius R fit in a row above it — the areas on screen are the ones
// in the argument, not a convenient cartoon.
rect(sheet, (640, 430), 691, 220); outlined(sheet); outline(sheet, dim); hidden(sheet);
text(wlab, (640, 566), "2πR"); display(wlab); size(wlab, 22); color(wlab, cyan); hidden(wlab);
text(hlab, (250, 430), "2R"); display(hlab); size(hlab, 22); color(hlab, magenta); hidden(hlab);
equation(area, (640, 214), `2\pi R \cdot 2R = 4\pi R^2`, 34); color(area, gold); hidden(area);
// Four discs, each unrolled ring by ring into a right triangle that lands in the sheet: a
// ring of radius r straightens into a segment 2*pi*r long, so the stack of them IS a
// triangle of base 2piR, height R, area piR^2. Two of them tile each half of the sheet.
// 12,000 points as 300 angles x 40 radii, so the long outer rings stay solid when straight.
parameter(un, (150, 606), 0, 0, 1, "unrolled", 2); hidden(un.widget);
cloud(d0, 12000, cyan) {
let r = (mod(i, 40) + 0.5)/40;
let th = floor(i/40) * 0.020944;
let x = (1-un)*(200 + 110*r*cos(th)) + un*(294 + 110*r*th);
let y = (1-un)*(170 + 110*r*sin(th)) + un*(320 + 110*r);
let rr = 1.5;
let hue = 196;
}
cloud(d1, 12000, cyan) {
let r = (mod(i, 40) + 0.5)/40;
let th = floor(i/40) * 0.020944;
let x = (1-un)*(420 + 110*r*cos(th)) + un*(985 - 110*r*th);
let y = (1-un)*(170 + 110*r*sin(th)) + un*(430 - 110*r);
let rr = 1.5;
let hue = 220;
}
cloud(d2, 12000, cyan) {
let r = (mod(i, 40) + 0.5)/40;
let th = floor(i/40) * 0.020944;
let x = (1-un)*(640 + 110*r*cos(th)) + un*(294 + 110*r*th);
let y = (1-un)*(170 + 110*r*sin(th)) + un*(430 + 110*r);
let rr = 1.5;
let hue = 288;
}
cloud(d3, 12000, cyan) {
let r = (mod(i, 40) + 0.5)/40;
let th = floor(i/40) * 0.020944;
let x = (1-un)*(860 + 110*r*cos(th)) + un*(985 - 110*r*th);
let y = (1-un)*(170 + 110*r*sin(th)) + un*(540 - 110*r);
let rr = 1.5;
let hue = 324;
}
hidden(d0); hidden(d1); hidden(d2); hidden(d3);
// ---- the lemma, as a cross-section ----
circle(cs, (420, 380), 150); outlined(cs); outline(cs, dim); hidden(cs);
line(axis, (420, 200), (420, 560)); color(axis, dim); hidden(axis);
line(wall, (570, 200), (570, 560)); color(wall, cyan); hidden(wall);
line(ray, (420, 380), (570, 275)); color(ray, gold); untraced(ray);
line(dseg, (420, 294), (543, 294)); color(dseg, magenta); untraced(dseg);
dot(tile, (543, 294), 5); color(tile, cyan); hidden(tile);
text(dlab, (478, 270), "d"); display(dlab); size(dlab, 20); color(dlab, magenta); hidden(dlab);
text(rlab, (492, 352), "R"); display(rlab); size(rlab, 20); color(rlab, gold); hidden(rlab);
equation(wide, (860, 320), `\text{width} \times \tfrac{R}{d}`, 30); color(wide, cyan); hidden(wide);
equation(short, (860, 396), `\text{height} \times \tfrac{d}{R}`, 30); color(short, magenta); hidden(short);
equation(one, (860, 480), `\tfrac{R}{d}\cdot\tfrac{d}{R}=1`, 30); color(one, gold); hidden(one);
// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.4);
show(cap, 0.3);
say(cap, "A sphere of radius R. Roll it in your hand: how much surface is there?");
show(shell, 0.9);
show(floor, 0.5);
orbit3(34, 22, 4.6, 2.6, smooth);
wait(0.8);
par { fade(ttl, 0.8); fade(sub, 0.8); }
say(cap, "Here is its shadow — a circle of area πR². The sphere's area is exactly four of those.");
show(shade, 0.8);
pulse(shade, 0.9);
wait(2.2);
say(cap, "Four. Not π, not 2π. Four circles' worth of paper, wrapped on a ball. Why?");
wait(2.4);
// ================================= ACT II =================================
say(act, "II · onto a cylinder");
show(act, 0.4);
par { fade(shade, 0.6); fade(floor, 0.5); }
say(cap, "Cut the surface into tiles. Nothing about the sphere has changed yet.");
par { fade(shell, 0.7); show(tiles, 0.9); }
wait(1.6);
say(cap, "Now push every tile straight out from the axis, onto the cylinder that encloses it.");
show(map.widget, 0.5);
to(map, value, 1, 3.2, smooth);
wait(1.0);
say(cap, "Watch one band. It moved out, so it got wider — and it tilted flat, so it got shorter.");
pulse(tiles.row7, 0.9);
wait(2.2);
say(cap, "That trade is exact. Cut the ball in half and it is two similar triangles.");
par {
fade(tiles, 0.8);
fade(map.widget, 0.5);
}
show(cs, 0.6);
show(axis, 0.5);
show(wall, 0.6);
wait(0.6);
draw(ray, 0.7);
draw(dseg, 0.5);
show(tile, 0.4);
show(dlab, 0.4);
show(rlab, 0.4);
wait(1.4);
say(cap, "A tile at distance d from the axis lands at distance R, so its width scales by R/d.");
show(wide, 0.7);
wait(2.0);
say(cap, "And the surface there leans by the same ratio, so its height squishes by d/R.");
show(short, 0.7);
wait(2.0);
say(cap, "One stretch, one squish, the same number. The tile's AREA never changed.");
show(one, 0.8);
wait(2.4);
// ================================= ACT III =================================
say(act, "III · unwrap it");
par {
fade(cs, 0.6); fade(axis, 0.5); fade(wall, 0.5); fade(ray, 0.5); fade(dseg, 0.5);
fade(dlab, 0.4); fade(rlab, 0.4); fade(tile, 0.4); fade(wide, 0.6); fade(short, 0.6); fade(one, 0.6);
}
say(cap, "So the sphere and the cylinder have the same area — and a cylinder unrolls flat.");
show(tiles, 0.8);
orbit3(-96, 54, 6.4, 2.4, smooth);
to(map, value, 2, 3.0, smooth);
wait(1.2);
say(cap, "A rectangle. Its height is 2R, and its width is the cylinder's circumference, 2πR.");
wait(2.4);
par { fade(tiles, 0.9); }
show(sheet, 0.7);
show(wlab, 0.5);
show(hlab, 0.5);
wait(1.0);
show(area, 0.9);
say(cap, "Two π R, times two R. Four π R squared — the sphere's area, with nothing left over.");
wait(2.8);
// ================================= ACT IV =================================
say(act, "IV · and the four circles");
fade(area, 0.7);
say(cap, "One thing is still owed: why FOUR circles fill that rectangle. Here are four.");
par {
show(d0, 0.6); show(d1, 0.6); show(d2, 0.6); show(d3, 0.6);
}
wait(1.6);
say(cap, "Unroll each one ring by ring. A ring of radius r straightens into a line 2πr long.");
show(un.widget, 0.5);
to(un, value, 1, 3.4, smooth);
wait(1.0);
say(cap, "Each circle becomes a right triangle: base 2πR, height R, area πR². Four of them —");
wait(2.4);
say(cap, "— and they tile the rectangle exactly. A sphere is four of its own shadows.");
show(area, 0.9);
wait(3.0);
// ================================= ENDCARD =================================
par {
fade(d0, 0.8); fade(d1, 0.8); fade(d2, 0.8); fade(d3, 0.8);
fade(sheet, 0.6); fade(wlab, 0.5); fade(hlab, 0.5); fade(area, 0.8);
fade(un.widget, 0.5); fade(cap, 0.7); fade(act, 0.6);
}
text(end1, (640, 340), "Push it out, unroll it, count the circles.");
display(end1); size(end1, 42); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
quaternions
Quaternions, as 3Blue1Brown builds them in _2018/quaternions.py — not a formula with four
letters in it, but four pictures, each the last one with a dimension added. ACT I warp shows
multiplying by 1+i turning AND stretching the plane, then a unit number turning it rigidly. ACT II
a circle becomes a line: the pole, three construction rays, and 520 beads sliding out to 2·tan(a/2).
ACT III the same thing one dimension up — a wire globe (param3 + bind) peels off its pole and
lies down as an honest disc. ACT IV the unit quaternions ARE a 3-sphere, so it has no picture: 6,000
points on twelve linked HOPF FIBRES, stereographically projected, left-multiplied by cos θ + j sin θ
from one dial. Every act is driven by a parameter, not by the clock — including the 3-sphere, whose
cloud is a closed-form function of (i, dial): a Hamilton product, then a projection.
// quaternions — the argument 3Blue1Brown builds in `_2018/quaternions.py`, which is not
// "here is a formula with four letters in it" but a chain of four pictures, each one the
// last one with a dimension added:
//
// ACT I multiplying complex numbers TURNS the plane (`warp`, z → q·z)
// ACT II a circle is a line, seen from the pole (stereographic, 2-D)
// ACT III a sphere is a plane, seen from the pole (`param3` + `bind`)
// ACT IV the unit quaternions are a 3-SPHERE — project it into 3-space and left
// multiplication becomes a visible flow (`cloud3` + `parameter`)
//
// The point of the sequence is that you never see four dimensions; you see a shadow of
// them, and multiplication is a rigid turn of the thing casting it.
//
// Everything here is driven by scene `parameter`s rather than by time, so each picture is
// a DIAL you can stop anywhere — including the 3-sphere, whose 5,000 particles are a
// closed-form function of (i, dial): a Hamilton product, then a projection.
//
// manic examples/quaternions.manic
title("Quaternions — Turning in Four Dimensions");
canvas("16:9");
template("black");
bloom(0.32, 0.55, 24);
// ---- type ----
text(ttl, (640, 92), "Quaternions");
display(ttl); size(ttl, 54); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 148), "turning in four dimensions");
display(sub); size(sub, 23); color(sub, dim); hidden(sub);
text(cap, (640, 662), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1122, 620), ""); display(act); size(act, 19); color(act, gold); hidden(act);
// ============================ ACT I — a turn of the plane ============================
// z → (1+i)z turns AND stretches; z → (cos60 + i sin60)z only turns, because |q| = 1.
warp(gstretch, (770, 366), 64, `(1+i)*z`, 3, 34); color(gstretch, dim); hidden(gstretch);
warp(gturn, (770, 366), 64, `(0.5+0.8660254*i)*z`, 3, 34); color(gturn, cyan); hidden(gturn);
equation(eq1, (228, 340), `z \mapsto (1+i)\,z`, 30); color(eq1, gold); hidden(eq1);
equation(eq2, (228, 340), `z \mapsto (\cos 60^{\circ}+i\sin 60^{\circ})\,z`, 26);
color(eq2, gold); hidden(eq2);
// ============================ ACT II — the circle and the line ======================
parameter(flat, (168, 596), 0, 0, 1, "projected", 2); hidden(flat.widget);
circle(hoop, (640, 300), 140); outlined(hoop); outline(hoop, dim); hidden(hoop);
line(axis, (140, 440), (1140, 440)); color(axis, dim); hidden(axis);
dot(pole, (640, 160), 6); color(pole, gold); hidden(pole);
text(plab, (640, 132), "the pole you look from");
display(plab); size(plab, 17); color(plab, gold); hidden(plab);
// three rays of the construction: from the pole, through a point of the circle, to the line
line(ray1, (640, 160), (831, 440)); color(ray1, magenta); untraced(ray1);
line(ray2, (640, 160), (448, 440)); color(ray2, magenta); untraced(ray2);
line(ray3, (640, 160), (1032, 440)); color(ray3, magenta); untraced(ray3);
// the circle's own points, which slide out along those rays as `flat` opens
// x_line = 2·tan(a/2): a point near the pole lands far away, and the pole itself never lands
cloud(beads, 520, cyan) {
let a = -2.0 + (i/520)*4.0;
let px = sin(a);
let py = -cos(a);
let xl = 2*sin(a)/(1 + cos(a));
let x = 640 + 140*(px*(1-flat) + xl*flat);
let y = 300 - 140*(py*(1-flat) - flat);
let r = 2.4;
let hue = 188 + 18*flat;
}
hidden(beads);
// ============================ ACT III & IV — the 3-D stage =========================
camera3((4.8, -5.4, 3.6), (0, 0, 0), 44, perspective);
// The sphere, morphing into its own stereographic projection — the same picture as ACT II
// with one more dimension, and the same dial. It is parametrized AROUND the pole you look
// from (`v` is the angle away from it, stopping just short at 2.45 rad), so the flattened
// picture is an honest disc: radius tan(v/2), the pole itself infinitely far out.
parameter(proj, (168, 596), 0, 0, 1, "projected", 2); hidden(proj.widget);
param3(ball,
"(1-p)*sin(v)*cos(u) + 0.4*p*sin(v)*cos(u)/(1+cos(v))",
"(1-p)*sin(v)*sin(u) + 0.4*p*sin(v)*sin(u)/(1+cos(v))",
"(1-p)*cos(v)",
(0, tau), (0.16, 2.45), 26);
bind(proj, ball, formula,
"(1-p)*sin(v)*cos(u) + 0.4*p*sin(v)*cos(u)/(1+cos(v))",
"(1-p)*sin(v)*sin(u) + 0.4*p*sin(v)*sin(u)/(1+cos(v))",
"(1-p)*cos(v)");
color(ball, cyan); finish3(ball, "wire=1"); hidden(ball); // a wire globe reads as a GRID, and the grid is what gets carried to the plane
// THE 3-SPHERE. Every unit quaternion q = w + xi + yj + zk with |q| = 1 lives on it: a
// 3-dimensional surface in 4-space, so it has no picture — but its shadow in 3-space does.
// The twelve circles below are HOPF FIBRES over a ring of directions: great circles of the
// 3-sphere, every pair of them linked, which stereographic projection carries to linked
// circles here. Each is sampled by ARC LENGTH (the `atan2` reparametrization), or the
// projection would bunch every dot at the near side. `dial` left-multiplies all 6,000 of
// them by cos θ + j sin θ — a rigid turn of the 3-sphere, which the shadow has to bend to
// follow. The pole is set just outside (`d = rw + 1.12`) so no circle ever runs off to
// infinity mid-turn.
parameter(dial, (150, 596), 0, 0, 1, "θ", 2); hidden(dial.widget);
cloud3(s3, 6000, #00e5ff, 0.6) {
let f = mod(i, 12); // which fibre
let a0 = 0.95; // the ring of directions they sit over
let ph = f * 0.5235988; // where this one sits around that ring
let u = floor(i/12) * 0.0125664; // 500 samples along the fibre
let c = cos(a0);
let ec = sqrt((1+c)/(1-c));
let sp = 2*atan2(ec*sin(u/2), cos(u/2)); // uniform spacing AFTER projection
let qw = c*cos(sp); // the fibre itself: a great circle of S³
let qx = c*sin(sp);
let qy = sin(a0)*cos(sp + ph);
let qz = sin(a0)*sin(sp + ph);
let ang = dial * tau;
let m0 = cos(ang);
let m2 = sin(ang);
let rw = m0*qw - m2*qy; // the Hamilton product (cos θ + j sin θ)·q
let rx = m0*qx + m2*qz;
let ry = m0*qy + m2*qw;
let rz = m0*qz - m2*qx;
let d = rw + 1.12;
let x = rx / d;
let y = ry / d;
let z = rz / d;
let r = 0.013;
let hue = 186 + 100*f/12;
let alpha = 0.6;
}
glow(s3, 2); hidden(s3);
equation(ham, (250, 210), `i^2=j^2=k^2=ijk=-1`, 27); color(ham, gold); hidden(ham);
text(hlab, (250, 262), "Hamilton, on a bridge in Dublin, 1843");
display(hlab); size(hlab, 17); color(hlab, dim); hidden(hlab);
// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.5);
show(cap, 0.3);
say(cap, "Start in the complex plane, where multiplying does something to ALL of it.");
par { fade(ttl, 0.8); fade(sub, 0.8); }
show(gstretch, 0.7);
show(eq1, 0.6);
wait(0.6);
to(gstretch, morph, 1, 2.0, smooth);
wait(0.9);
say(cap, "Multiply by 1+i and the plane turns — and stretches, since 1+i is longer than 1.");
wait(2.0);
par { fade(gstretch, 0.6); fade(eq1, 0.5); }
say(cap, "Pick a number of length exactly one, and the stretching stops.");
show(gturn, 0.6);
show(eq2, 0.6);
to(gturn, morph, 1, 2.2, smooth);
wait(1.4);
say(cap, "A unit complex number IS a rotation. That is the whole idea — the rest is dimensions.");
wait(2.6);
// ================================= ACT II =================================
par { fade(gturn, 0.8); fade(eq2, 0.6); }
say(act, "II · a circle is a line");
show(act, 0.4);
say(cap, "Before four dimensions, do two. Here is a circle, and a line it just touches.");
show(hoop, 0.7);
show(axis, 0.6);
show(beads, 0.7);
wait(1.4);
say(cap, "Stand at the top. Look through any point of the circle, and you land on the line.");
show(pole, 0.5);
show(plab, 0.4);
par { draw(ray1, 0.6); draw(ray2, 0.6); draw(ray3, 0.7); }
wait(1.8);
say(cap, "Every point of the circle has its own place on the line — so let them go there.");
show(flat.widget, 0.5);
to(flat, value, 1, 2.6, smooth);
wait(1.0);
say(cap, "The circle became the line. Only the pole is missing — it would land infinitely far.");
wait(2.4);
say(cap, "One missing point, in exchange for a flat picture. It works in any dimension.");
wait(2.6);
// ================================= ACT III =================================
par {
fade(beads, 0.8); fade(hoop, 0.6); fade(axis, 0.6);
fade(ray1, 0.5); fade(ray2, 0.5); fade(ray3, 0.5);
fade(pole, 0.5); fade(plab, 0.5); fade(flat.widget, 0.5);
}
say(act, "III · a sphere is a plane");
say(cap, "One dimension up: a sphere, and the same pole to look from.");
show(ball, 0.9);
orbit3(28, 22, 4.8, 2.6, smooth);
wait(1.0);
say(cap, "Open the same dial. The sphere peels off the pole and lies down flat.");
show(proj.widget, 0.5);
to(proj, value, 1, 3.0, smooth);
wait(1.2);
say(cap, "A sphere is a plane plus one point. Nothing tore; the pole was sent away.");
orbit3(64, 62, 4.9, 3.0, smooth);
wait(2.2);
// ================================= ACT IV =================================
par { fade(ball, 0.9); fade(proj.widget, 0.5); }
say(act, "IV · the unit quaternions");
say(cap, "Now four. Take every quaternion of length one: w² + x² + y² + z² = 1.");
show(ham, 0.7);
show(hlab, 0.5);
wait(1.8);
say(cap, "A three-dimensional surface in four-dimensional space: a 3-sphere. It has no picture.");
wait(2.2);
say(cap, "But it has a shadow. Project from a pole, as before, and it fits in this room.");
par {
fade(ham, 0.8);
fade(hlab, 0.6);
}
show(s3, 1.2);
orbit3(-24, 22, 4.6, 3.2, smooth);
wait(1.2);
say(cap, "Six thousand of them, on twelve great circles — and every pair is linked.");
wait(2.2);
say(cap, "Multiply every one by cos θ + j sin θ — Act I's move, one dimension up.");
show(dial.widget, 0.6);
to(dial, value, 0.5, 4.0, smooth);
wait(0.4);
say(cap, "Nothing is being deformed. The 3-sphere is turning rigidly; only its shadow bends.");
to(dial, value, 1, 4.0, smooth);
wait(0.6);
say(cap, "Half a turn of the dial sent 1 to −1. A full turn brings every point home.");
orbit3(78, -18, 5.0, 4.0, smooth);
wait(2.0);
say(cap, "A quaternion multiplication: a rotation you can only watch in shadow.");
wait(2.6);
// ================================= ENDCARD =================================
par {
fade(s3, 1.2);
fade(dial.widget, 0.6);
fade(cap, 0.7);
fade(act, 0.6);
}
text(end1, (640, 336), "Four dimensions, watched from three.");
display(end1); size(end1, 44); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 416), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
dominos-one-push
The whole domino story in one film, from 3Blue1Brown’s 2017 experiments. ACT I a row places itself and one push crosses it (side on, so you watch each slab turn). ACT II why it works at all - the balance angle atan(t/h) it must climb past, the contact angle asin(s/h) it must reach. ACT III it doesn’t have to be a line: a spiral then a heart, seen from above, each placed one domino at a time. ACT IV the proof - twelve chains swept over gap x per-impact loss, where a gap taller than the domino and a too-lossy surface both kill the wave. Nothing in the file sets the speed; it emerges.
// dominos-one-push — the whole domino story in one film.
//
// Built from 3Blue1Brown's 2017 `dominos/` material, which is not an animation of falling
// dominos at all: it is 19 real experiments (frame numbers at 1000–5000 fps, one per tap)
// measuring how fast a toppling wave travels, plus a geometry scene showing how far a
// domino must tip to reach the next one. That geometry is the whole model here.
//
// ACT I a row places itself, then one push crosses it (`dominos`, side on)
// ACT II why it works at all — balance angle, contact angle
// ACT III it doesn't have to be a line: a spiral, then a heart (`dominopath`, top down)
// ACT IV the proof — twelve chains over spacing x loss, some of which die (`sweep`)
//
// Every domino is a rigid slab pivoting on its leading base edge: it must be pushed past
// atan(t/h) before gravity helps, and it reaches its neighbour at asin(s/h). Contact is
// sustained, so the whole leaning group drives the wave. Nothing in this file sets the
// wave's speed — it emerges from spacing, height, thickness and the per-impact loss.
// Lengths are in millimetres, so the experiment's own numbers go straight in.
//
// manic examples/dominos-one-push.manic
title("Dominos — One Push");
canvas("16:9");
template("black");
bloom(0.36, 0.52, 26);
// ---- the room: a dark tabletop, light falling from above ----
shader(room) {
let x = (u - 0.5) * asp;
let y = v - 0.5;
let d = sqrt(x*x + y*y);
let pool = 1.0 - smoothstep(0.06, 0.78, d);
let grain = 0.5 + 0.5*fbm(u*9.0, v*9.0);
let hue = 214 - 8.0*pool;
let sat = 0.55 - 0.25*pool;
let val = 0.012 + 0.055*pool + 0.012*grain*pool;
}
z(room, -10);
// ---- type ----
text(ttl, (640, 96), "Dominos — One Push");
display(ttl); size(ttl, 46); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 152), "and nothing here decides how fast it travels");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 656), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(note, (640, 618), ""); display(note); size(note, 19); color(note, gold); hidden(note);
// ---- ACT I — the row (side on: you can watch each slab turn) ----
dominos(row, (470, 386), 16, 45, 7.5438, 9.38, 2, 0.62, 7);
color(row, cyan);
color(row.ground, dim); // the table is furniture, not neon
color(row.d0, gold);
hidden(row.dominos);
framebox(mark, row.d0, 7); color(mark, gold); untraced(mark);
// ---- ACT II — the geometry, beside the row ----
equation(contact, (988, 330), `\theta_c=\arcsin\frac{s}{h}=12^{\circ}`, 27);
color(contact, gold); hidden(contact);
equation(balance, (988, 404), `\theta_b=\arctan\frac{t}{h}=9.5^{\circ}`, 27);
color(balance, magenta); hidden(balance);
text(geo1, (988, 462), "tip past 9.5° or it stands back up");
display(geo1); size(geo1, 18); color(geo1, dim); hidden(geo1);
text(geo2, (988, 492), "reach 12° and the next one goes");
display(geo2); size(geo2, 18); color(geo2, dim); hidden(geo2);
// ---- ACT III — the same physics, any shape (top down) ----
dominopath(spiral, (640, 380), 12.6, 12.6, "t*cos(t)", "t*sin(t)", 92, (1.4, 18.6), 45, 7.5438, 1.7, 0.7, 8);
hidden(spiral.dominos); untraced(spiral.path);
dominopath(heart, (640, 366), 12.2, 12.2,
"16*sin(t)^3", "13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)", 56, (0, 6.2832), 45, 7.5438, 1.7, 0.7, 8);
color(heart, magenta);
hidden(heart.dominos); untraced(heart.path);
// ---- ACT IV — twelve chains: gap across, per-impact loss down ----
dominos(cellrow, (0, 0), 10, 45, 7.5438, 9.38, 1.05, 0.6, 6); hidden(cellrow);
sweep(grid, cellrow, spacing, (9, 46), transfer, (0.95, 0.35), (640, 404), 4, 3, 250, 128, 0, 0);
hidden(grid);
// ============================ ACT I ============================
show(ttl, 0.8);
show(sub, 0.7);
show(cap, 0.3);
wait(0.5);
say(cap, "Sixteen slabs, forty-five millimetres tall, nine point four apart.");
show(row.ground, 0.5);
stagger(0.055) {
for i in 0..16 {
show(row.d{i}, 0.22);
}
}
wait(0.4);
draw(mark, 0.5);
say(cap, "One nudge, on that one.");
pulse(row.d0, 0.6);
wait(0.3);
par {
fade(ttl, 0.8);
fade(sub, 0.8);
}
run(row, 4.4);
wait(0.5);
say(cap, "The wave crossed the row. Its speed was never written down.");
wait(1.6);
// ============================ ACT II ============================
say(cap, "Each slab has to climb past its own balance angle before gravity takes over.");
show(balance, 0.7);
show(geo1, 0.5);
wait(1.8);
say(cap, "Then it only has to reach the next one — twelve degrees, for these dominos.");
show(contact, 0.7);
show(geo2, 0.5);
wait(2.2);
say(cap, "Two angles, and the whole cascade follows.");
wait(1.8);
// ============================ ACT III ============================
par {
fade(row, 0.8);
fade(mark, 0.5);
fade(contact, 0.7);
fade(balance, 0.7);
fade(geo1, 0.5);
fade(geo2, 0.5);
}
say(cap, "Nothing about that argument needs a straight line. Seen from above —");
draw(spiral.path, 1.3, smooth);
show(note, 0.4);
say(note, "r = t");
stagger(0.016) {
for i in 0..92 {
show(spiral.d{i}, 0.16);
}
}
wait(0.5);
say(cap, "Ninety-two dominos on a spiral, standing at equal spacing along the curve.");
run(spiral, 5.4);
wait(0.9);
par { fade(spiral, 0.8); fade(spiral.path, 0.6); }
say(cap, "Change the formula. Keep the physics.");
draw(heart.path, 1.1, smooth);
say(note, "x = 16 sin³t, y = 13 cos t − 5 cos 2t − 2 cos 3t − cos 4t");
size(note, 17);
stagger(0.022) {
for i in 0..56 {
show(heart.d{i}, 0.18);
}
}
wait(0.4);
say(cap, "A closed curve, so the wave runs all the way round and meets where it began.");
run(heart, 5.0);
wait(1.4);
// ============================ ACT IV ============================
par {
fade(heart, 0.8);
fade(heart.path, 0.6);
fade(note, 0.5);
}
say(cap, "So what does set the speed? Only the geometry — here it is, twelve times over.");
par {
show(grid.chrome, 0.6);
show(grid.headers, 0.7);
}
show(grid.cells, 0.9);
wait(0.5);
run(grid, 7.5);
wait(0.8);
say(cap, "Wider gaps run faster — until the gap is taller than the domino. Then nothing arrives.");
wait(2.2);
say(cap, "Bottom left dies too: that surface loses too much at every impact.");
wait(2.4);
// ============================ ENDCARD ============================
par {
fade(grid, 0.9);
fade(cap, 0.7);
}
text(end1, (640, 344), "One push. The rest is geometry.");
display(end1); size(end1, 46); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 424), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
dominos-any-shape
Dominos v2 — the same slab physics standing along ANY parametric curve, seen from above (a standing
domino is a short bar, a fallen one a long bar lying forward). A spiral, a heart and a 1:2 lissajous,
each PLACED ONE BY ONE with a staggered loop over the run’s own pieces, then toppled with one push.
count sets the spacing, so more dominos means tighter gaps and a slower wave; closed curves topple
all the way round. Nothing sets the wave speed - it emerges from the geometry.
// dominos-any-shape — dominos v2: stand them along ANY curve, place them one by one,
// then push the first one over.
//
// v1 (`examples/dominos.manic`) is the side view of a straight row, built from
// 3Blue1Brown's 2017 domino experiments. This is the same slab physics seen from
// ABOVE — where a standing domino is a short bar and a fallen one is a long bar lying
// forward along the path — which is what makes an arbitrary arrangement legible.
//
// The shape is a `param`-style formula pair, so anything you can write, you can topple:
// a spiral, a heart, a figure of eight. `count` sets the spacing (equal arc length), so
// asking for more dominos tightens the gaps and slows the wave; the physics is unchanged
// (balance angle atan(t/h), contact at asin(s/h), sustained contact carrying the wave)
// and nothing sets the speed — it emerges from the geometry.
//
// The placing-one-by-one beat needs no new vocabulary: the run's pieces are ordinary
// entities `{id}.d{i}`, so `stagger` over a `for` loop lays them down in order.
//
// manic examples/dominos-any-shape.manic
title("Dominos, any shape you like");
canvas("16:9");
template("black");
bloom(0.3, 0.5, 20);
text(head, (640, 70), "Dominos, any shape you like");
display(head); size(head, 34); bold(head); color(head, fg); hidden(head);
text(cap, (640, 660), ""); display(cap); size(cap, 23); color(cap, dim); hidden(cap);
text(shape, (640, 618), ""); display(shape); size(shape, 20); color(shape, gold); hidden(shape);
// ---- three arrangements, same physics ----
// a spiral: r = t, so the gap between turns stays constant
dominopath(spiral, (640, 372), 13, 13, "t*cos(t)", "t*sin(t)", 96, (1.4, 19.2), 45, 7.5438, 1.7, 0.7, 8);
hidden(spiral.dominos); untraced(spiral.path); // pieces fade in; the guide is armed for draw-on
// a heart — the classic parametric one, closed, so the wave runs all the way round
dominopath(heart, (640, 356), 12.5, 12.5,
"16*sin(t)^3", "13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)", 58, (0, 6.2832), 45, 7.5438, 1.7, 0.7, 8);
hidden(heart.dominos); untraced(heart.path); // pieces fade in; the guide is armed for draw-on
// a figure of eight: a lissajous with a 1:2 frequency ratio
dominopath(eight, (640, 372), 300, 150, "sin(t)", "sin(2*t)", 84, (0, 6.2832), 45, 7.5438, 1.7, 0.72, 8);
hidden(eight.dominos); untraced(eight.path); // pieces fade in; the guide is armed for draw-on
// ---- Act 1: the spiral, laid down one domino at a time ----
show(head, 0.6);
show(cap, 0.3);
say(cap, "Ninety-six dominos, standing along a spiral. Watch them go down one by one.");
draw(spiral.path, 1.2, smooth);
show(shape, 0.4);
say(shape, "r = t");
stagger(0.017) {
for i in 0..96 {
show(spiral.d{i}, 0.16);
}
}
wait(0.5);
say(cap, "One push at the middle, and the wave winds outward. Nothing here sets its speed.");
run(spiral, 5.5);
wait(0.8);
// ---- Act 2: the same physics, a heart ----
par { fade(spiral, 0.7); fade(spiral.path, 0.5); }
say(cap, "Change the formula, keep the physics. A closed curve topples all the way round.");
draw(heart.path, 1.0, smooth);
say(shape, "x = 16 sin³t, y = 13 cos t − 5 cos 2t − 2 cos 3t − cos 4t");
size(shape, 17);
stagger(0.022) {
for i in 0..58 {
show(heart.d{i}, 0.18);
}
}
wait(0.4);
run(heart, 5.0);
wait(0.8);
// ---- Act 3: a figure of eight, and the wave crosses its own middle ----
par { fade(heart, 0.7); fade(heart.path, 0.5); }
say(cap, "A lissajous at 1:2 — the run crosses itself, and the wave passes straight through.");
say(shape, "x = sin t, y = sin 2t");
size(shape, 20);
draw(eight.path, 1.0, smooth);
stagger(0.018) {
for i in 0..84 {
show(eight.d{i}, 0.16);
}
}
wait(0.4);
run(eight, 5.0);
wait(0.6);
say(cap, "Same slab, same contact angle, same emergent speed — only the path changed.");
wait(2.0);
dominos
A toppling wave, and what sets its speed — from 3Blue1Brown’s 2017 domino EXPERIMENTS (19 data
files of frame numbers at 1000-5000 fps). Each domino is a rigid slab pivoting on its base edge, so
it must tip past its balance angle atan(t/h) before gravity helps and reaches its neighbour at the
contact angle asin(spacing/height); contact is SUSTAINED, so the leaning group carries the wave.
Nothing sets the speed — it emerges. Lengths are in millimetres, so the experiment’s own numbers go
in (45 tall, 7.5438 thick, 9.38 apart). Act 2 is the same row rebuilt by sweep over spacing x
per-impact loss: twelve chains, some of which never finish.
// dominos — a toppling wave, and what sets its speed.
//
// From 3Blue1Brown's 2017 `dominos/domino_play.py`, which is not an animation of falling
// dominos at all: it plots 19 real experiments (frame numbers at 1000–5000 fps, one per
// tap) to measure how fast the toppling wave travels, and a `Test` scene that draws the
// geometry — how far a domino must tip before it reaches the next one.
//
// That geometry is the whole model. Each domino is a rigid slab pivoting on its leading
// base edge, so it must be pushed past its balance angle atan(t/h) before gravity helps,
// and it reaches its neighbour at the contact angle asin(spacing/height) — the arc 3b1b
// draws. Contact is SUSTAINED: the faller leans on the next one and keeps driving it, so
// the weight of the whole leaning group carries the wave. Nothing here sets the speed
// directly: it emerges from spacing, height, thickness and the per-impact loss.
//
// Lengths are in millimetres, so the experiment's own numbers go straight in — 45 mm
// tall, 7.5438 mm thick, 9.38 mm apart is 3b1b's main dataset.
//
// manic examples/dominos.manic
title("Dominos — the wave nobody sets the speed of");
canvas("16:9");
template("black");
// ---- the row: real domino numbers, tagged so core verbs reach it ----
dominos(row, (470, 300), 14, 45, 7.5438, 9.38, 2, 0.6, 6);
color(row, cyan); // one verb, the whole row
color(row.d0, gold); // ...or one piece
framebox(first, row.d0, 7); color(first, gold); untraced(first);
text(head, (640, 74), "One push, fourteen dominos");
display(head); size(head, 34); bold(head); color(head, fg); hidden(head);
text(cap, (640, 654), ""); display(cap); size(cap, 24); color(cap, dim); hidden(cap);
equation(geo, (950, 250), `\theta_c=\arcsin\!\frac{s}{h}=12^{\circ}`, 26);
color(geo, gold); hidden(geo);
equation(bal, (950, 320), `\theta_b=\arctan\!\frac{t}{h}=9.5^{\circ}`, 26);
color(bal, magenta); hidden(bal);
text(geonote, (950, 392), "tip past 9.5° or it stands back up");
display(geonote); size(geonote, 19); color(geonote, dim); hidden(geonote);
// ---- the grid: the same row rebuilt over spacing × per-impact loss ----
// `sweep` re-invokes the SAME constructor per cell, varying two of its own named
// parameters — so this grid is 12 independent chains, some of which die out.
dominos(cell, (0, 0), 10, 45, 7.5438, 9.38, 1.05, 0.6, 6); hidden(cell);
sweep(grid, cell, spacing, (9, 40), transfer, (0.9, 0.45), (640, 430), 4, 3, 250, 130, 0, 0);
hidden(grid);
text(gridhead, (640, 138), "Wider gaps run faster — until nothing arrives");
display(gridhead); size(gridhead, 27); bold(gridhead); color(gridhead, fg); hidden(gridhead);
text(gridnote, (640, 646), "");
display(gridnote); size(gridnote, 21); color(gridnote, dim); hidden(gridnote);
// ---- Act 1: one row falls ----
show(head, 0.6);
show(cap, 0.3);
say(cap, "Fourteen slabs, one nudge. Nothing in the file says how fast the wave travels.");
show(row, 0.7);
draw(first, 0.5);
wait(0.6);
run(row, 4.2);
wait(0.5);
say(cap, "Each slab has to tip past its own balance angle, then it reaches the next one.");
par { show(geo, 0.6); show(bal, 0.6); }
show(geonote, 0.5);
wait(2.6);
// ---- Act 2: the same row, swept over spacing and loss ----
par {
fade(row, 0.6);
fade(first, 0.4);
fade(cap, 0.4);
fade(geo, 0.5);
fade(bal, 0.5);
fade(geonote, 0.5);
fade(head, 0.5);
}
show(gridhead, 0.6);
show(gridnote, 0.3);
say(gridnote, "Twelve chains: gap across, per-impact loss down. Some never finish.");
par {
show(grid.chrome, 0.5);
show(grid.headers, 0.6);
}
show(grid.cells, 0.8);
wait(0.4);
run(grid, 7);
wait(0.6);
say(gridnote, "Speed is an outcome here, not a setting — the geometry decides it.");
wait(2.2);
cloud-ink-pair
Another @yuruyurau art-tweet in ONE cloud, staged on PAPER instead of in the dark: two fish
circling in ink. The whole trick is (i%2)*3 — every other point phase-shifted by 3 radians, so
one formula draws TWO animals orbiting a shared centre. template("paper"), no bloom, and the
ink pools dark along the bodies (alpha falls off with d) and dries to nothing at the fin
tips; p5’s semi-transparent stroke(w,96) was always ink. The near-vertical dotted streaks are
the original’s own 77 sin(e/2) passing through zero — its +1e-4 guard bounds the blow-up
instead of removing it, so they fall like rain. Pure in (i, t), so it scrubs.
// cloud-ink-pair — another @yuruyurau creature in ONE `cloud`, reimagined as a brush
// study: two fish circling on paper. The reference is a tweet-sized golf:
// k = 5cos(i/44), e = y/2-15, d = mag(k,e)/3, c = d/2 - t/3 + (i%2)·3, y = i/253
// point( (79 + d² + k²)·sin c + 200 + d³/4·cos(3t - d²/4) ,
// 99cos(c/2) + 4sin 2k + y/(77 sin(e/2) + 1e-4)·k·e + 200 )
// The whole trick is `(i%2)·3`: every other point is phase-shifted by 3 radians, so ONE
// formula draws TWO animals — a mirrored pair, orbiting a shared centre as `c` turns.
// The near-vertical dotted streaks are the original's own doing: `77 sin(e/2)` passes
// through zero, and the author's `+1e-4` guard bounds the blow-up instead of removing
// it. We keep the guard and the streaks; they fall like rain behind the pair.
//
// So this one is staged on PAPER rather than in the dark: `template("paper")`, no bloom,
// ink pooling dark along the bodies (`alpha` falls off with `d`) and drying to almost
// nothing at the fin tips. p5's semi-transparent `stroke(w,96)` was always ink.
//
// Faithful notes: p5's `mag` is `hypot`, `%` is `mod`, `**` is `^`; the p5 draw loop
// advances t by PI/80 per FRAME, so a frame-rate-free `t*1.0` stands in for it. Pure in
// (i, t) — it scrubs, seeks and records exactly, which the p5 original cannot do.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau).
//
// manic examples/cloud-ink-pair.manic
title("Two, in ink — a pair from one formula");
canvas("square");
template("paper");
// ---- the pair — `mod(i,2)` splits one formula into two animals ----
// Framing is the original's own, scaled: p5 draws into 400×400 about (200,200), so
// ×2.7 about (540,540) reproduces it at 1080, rain-streaks running off frame and all.
cloud(pair, 10000, #14141a, 0.95) {
let yy = i / 253.0;
let k = 5.0 * cos(i / 44.0);
let e = yy / 2.0 - 15.0;
let d = hypot(k, e) / 3.0;
let T = t * 1.0;
let c = d/2.0 - T/3.0 + mod(i, 2) * 3.0;
let px = (79.0 + d*d + k*k) * sin(c) + (d^3)/4.0 * cos(T*3.0 - d*d/4.0);
let py = 99.0*cos(c/2.0) + 4.0*sin(k*2.0) + yy/(77.0*sin(e/2.0) + 0.0001) * k * e;
let x = 540 + px * 2.7;
let y = 540 + py * 2.7;
// ink pools along the body, dries out toward the fins
let alpha = clamp(0.9 - d * 0.07, 0.16, 0.92);
let r = 1.3;
}
// ---- annotations ----
caption(head, "Two, in ink", (540, 96), 34); hidden(head);
caption(sub, "one formula, split by mod(i,2)", (540, 152), 21); hidden(sub);
equation(eq, (540, 946), `c=\tfrac{d}{2}-\tfrac{t}{3}+(i\bmod 2)\cdot 3`, 25); hidden(eq);
caption(lab, "manic", (540, 1006), 18); hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(24);
cloud-krill
Another @yuruyurau art-tweet in ONE cloud: a compact, densely bristled swimmer, pale and
cold-lit on a specimen plate. It is the SAME golf as cloud-crinoid with three constants
changed — y/5-13 for y/4-16, mag-4 for mag-5, and cos for sin in the last term —
which pulls the crinoid’s long sweeping arms into something stubby and coiled, breathing out of
phase with its own sweep. These tweet-sized formulas are a parameter space you can walk, not a
single drawing. Pure in (i, t), so it scrubs; the p5 original can’t.
// cloud-krill — another @yuruyurau creature in ONE `cloud`: a compact, densely bristled
// swimmer, pale and cold-lit. It is the same golf as `cloud-crinoid.manic` with three
// constants changed — worth knowing, because these tweet-sized formulas are a parameter
// space you can walk, not a single drawing:
//
// crinoid e = y/4 - 16 d = mag(k,e) - 5 + d²/3·sin(t - d²/7)
// this one e = y/5 - 13 d = mag(k,e) - 4 + d²/3·cos(t - d²/9)
//
// Everything else is identical — k = 4cos(i/29), c = d - t/3, y = i/295, and
// point( (d²/0.7 - 2k² + y)·cos c + 200 ,
// 3sin 2k + cos(y)/k + (y/9)k(3 + sin(9e - 3d + t)) + 79sin(c/3) + … + 200 )
// Shortening the spine (`-4`) and slowing the body taper (`y/5`) pulls the long sweeping
// arms in: where the crinoid fans wide, this is compact, dense and coiled — a stubbier
// relative on the same skeleton. The phase change from `sin` to `cos` in the last term
// re-times the breathing against the sweep, so the two never move alike.
//
// Staged as a specimen plate rather than a scene: no reef wall, no warm stone — a cold
// dark ground, a pale bone-blue body, and the three changed constants typeset below it.
//
// Faithful notes: p5's `mag` is `hypot`, `**` is `^`; the p5 draw loop advances t by
// PI/60 per FRAME, so a frame-rate-free `t*1.333` stands in for it (the crinoid's
// PI/40 became `t*2.0`, so the two run at the same relative pace). `cos(y)/k` keeps its
// division-by-almost-zero flecks. Pure in (i, t), so it scrubs, seeks and records.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau).
//
// manic examples/cloud-krill.manic
title("Krill — ten thousand points, one closed form");
canvas("square");
template("black");
bloom(0.3, 0.55, 22);
// ---- a cold, near-empty field: this is a plate, not a habitat ----
shader(field) {
let x = (u - 0.5) * asp;
let y = v - 0.5;
let d = sqrt(x*x + y*y);
let vig = 1.0 - 0.9*smoothstep(0.1, 0.75, d);
let hue = 214;
let sat = 0.5;
let val = 0.028 * vig + 0.008;
}
z(field, -10);
// ---- the animal — swept envelope is 222×459, so ×2.2 about (540,540) fills the plate ----
cloud(body, 10000, #ffffff, 0.34) {
let yy = i / 295.0;
let k = 4.0 * cos(i / 29.0);
let e = yy / 5.0 - 13.0;
let d = hypot(k, e) - 4.0;
let T = t * 1.333;
let c = d - T / 3.0;
let px = (d*d/0.7 - k*k*2.0 + yy) * cos(c);
let py = 3.0*sin(k*2.0) + cos(yy)/k + yy/9.0*k*(3.0 + sin(e*9.0 - d*3.0 + T)) + 79.0*sin(c/3.0) + (d*d)/3.0*cos(T - d*d/9.0);
let x = 540 + px * 2.2;
let y = 540 + py * 2.2;
// bone-blue: pale at the spine, cooling toward the barb tips
let hue = mod(206.0 + d * 2.2, 360);
let sat = clamp(0.06 + d * 0.05, 0.04, 0.5);
let val = clamp(0.74 + 0.26*sin(e*9.0 - d*3.0 + T), 0.36, 1.0);
let r = 1.25;
}
glow(body, 1);
// ---- annotations ----
caption(head, "Krill", (540, 96), 34); hidden(head);
caption(sub, "ten thousand points, one closed form", (540, 152), 21); hidden(sub);
equation(eq, (540, 950), `k=4\cos\tfrac{i}{29},\quad d=\mathrm{mag}\!\left(k,\tfrac{y}{5}-13\right)-4,\quad c=d-\tfrac{t}{3}`, 24); hidden(eq);
caption(lab, "manic", (540, 1010), 18); hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(24);
cloud-crinoid
Another @yuruyurau art-tweet in ONE cloud, reimagined as a gold feather star combing the
current: 10,000 points trace a curved spine with dozens of hooked barbs that sweep and re-comb
as the angle c = d - t/3 turns, over d = mag(4cos(i/29), y/4-16) - 5. The shader behind it
is the reef wall it clings to (mottled warm stone); the arms run bone at the spine → amber at the
barb tips, and the original’s cos(y)/k division-by-almost-zero survives as four drifting
flecks a frame. Pure in (i, t), so it scrubs; the p5 original can’t.
// cloud-crinoid — another @yuruyurau creature in ONE `cloud`, reimagined as a gold
// feather star combing the current. The reference is a tweet-sized golf:
// k = 4cos(i/29), e = y/4-16, d = mag(k,e)-5, c = d-t/3, y = i/295
// point( (d²/0.7 - 2k² + y)·cos c + 200 ,
// 3sin 2k + cos(y)/k + (y/9)k(3+sin(9e-3d+t)) + 79sin(c/3) + d²/3·sin(t-d²/7) + 200 )
// One curved spine with dozens of hooked barbs that sweep and re-comb as `c` turns —
// which is what a crinoid does for a living: perch on rock, fan its arms, strain the
// water. So the `shader` behind it is the reef wall it clings to (mottled warm stone),
// the arms run bone at the spine → amber at the barb tips, and `cos(y)/k` keeps its
// division-by-almost-zero spikes: four flecks a frame, drifting plankton.
//
// Faithful notes: p5's `mag` is `hypot`; `**` is `^`; the p5 draw loop advances t by
// PI/40 per FRAME, so a frame-rate-free `t*2.0` stands in for it. Pure in (i, t) —
// it scrubs, seeks and records exactly, which the p5 original cannot do.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau).
//
// manic examples/cloud-crinoid.manic
title("Crinoid — combing the current");
canvas("square");
template("black");
bloom(0.34, 0.55, 26);
// ---- the reef wall it perches on: mottled warm stone, darker toward the edges ----
shader(wall) {
let x = (u - 0.5) * asp;
let y = v - 0.5;
let d = sqrt(x*x + y*y);
let grain = 0.5 + 0.5*fbm(u*7.0, v*7.0);
let mott = 0.5 + 0.5*fbm(u*2.2 + 3.0, v*2.2);
let vig = 1.0 - 0.85*smoothstep(0.15, 0.72, d);
let hue = 28 + 10.0*mott;
let sat = 0.34 - 0.12*grain;
let val = (0.055 + 0.055*mott + 0.018*grain) * vig + 0.012;
}
z(wall, -10);
// ---- the animal — the yuruyurau golf, re-lit and framed ----
// The swept envelope of the formula is 315×185 wide over a full cycle of `c`, so
// scale 2.95 about (556, 435) centres it in the square at every t, not just at t=0.
cloud(arms, 10000, #ffffff, 0.34) {
let yy = i / 295.0;
let k = 4.0 * cos(i / 29.0);
let e = yy / 4.0 - 16.0;
let d = hypot(k, e) - 5.0;
let T = t * 2.0;
let c = d - T / 3.0;
let px = (d*d/0.7 - k*k*2.0 + yy) * cos(c);
let py = 3.0*sin(k*2.0) + cos(yy)/k + yy/9.0*k*(3.0 + sin(e*9.0 - d*3.0 + T)) + 79.0*sin(c/3.0) + d*d/3.0*sin(T - d*d/7.0);
let x = 556 + px * 2.95;
let y = 435 + py * 2.95;
// bone along the spine (small d) → amber where the barbs thin out (large d)
let hue = mod(44.0 - d * 1.1, 360);
let sat = clamp(0.10 + d * 0.045, 0.06, 0.62);
let val = clamp(0.72 + 0.28*sin(e*9.0 - d*3.0 + T), 0.34, 1.0);
let r = 1.25;
}
glow(arms, 1);
// ---- annotations ----
caption(head, "Crinoid", (540, 96), 34); hidden(head);
caption(sub, "one formula, ten thousand points", (540, 152), 21); hidden(sub);
equation(eq, (540, 946), `k=4\cos\tfrac{i}{29},\quad d=\mathrm{mag}\!\left(k,\tfrac{y}{4}-16\right)-5,\quad c=d-\tfrac{t}{3}`, 25); hidden(eq);
caption(lab, "manic", (540, 1006), 18); hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(24);
cloud-plumes
Another @yuruyurau art-tweet in ONE cloud: 30,000 points in two mirrored layers (mod(i,2)) drift
into flowing frond/plume forms and morph over time — a polar plot (radius q, angle c), hue-gradient
coloured and bloomed on a 9:16 Short. Pure in (i, t), so it scrubs; the p5 original can’t.
// cloud-plumes — another @yuruyurau art-tweet in ONE `cloud`: 30,000 points in
// two mirrored layers (`mod(i,2)`) drift into flowing frond/plume forms and morph
// over time. A polar plot — radius `q`, angle `c` — coloured per point and
// bloomed from the centre on a 9:16 Short. `mag(k,e)^2` becomes `k*k+e*e`, and
// the canvas `w` (=400) is folded into the constant `i/1200`.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau) — a prolific poster of
// tiny p5.js/dwitter art formulas. Our own hue'd, annotated take. Pure in (i, t),
// so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-plumes.manic
title("Two fronds from one formula");
canvas("9:16");
template("black");
cloud(fronds, 30000, #ffffff, 0.6) {
let m = mod(i, 2) * 3; // two layers: 0, 3
let k = 14*cos(i/39);
let e = i/1200 - 13;
let d = (k*k + e*e)/59 + 1;
let q = 89 - sin(k)*d + k*(8/d + sin(d*3 + e/9 - t));
let c = d*0.45 - sin(t - d)/8 - t/8 + m;
let px = q*sin(c);
let py = (q + 40 + 30*sin(c*2 + m))*cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.0 * grow;
let y = 960 + py * 2.0 * grow;
let hue = mod(m*70 + i*0.03 + t*15, 360);
}
// ---- textbook annotations ----
caption(head, "Two fronds from one formula", (540, 138), 36);
caption(sub, "30,000 points, no simulation", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q{+}\Delta)\cos c)`, 30);
caption(lab, "a polar plot: radius q, angle c, per point", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-shells
A tiny art-tweet by @yuruyurau, reimagined in ONE cloud: 10,000 points in three layers (mod(i,3))
placed by a polar formula (radius q, angle c), hue-gradient coloured and bloomed out of the centre on
a 9:16 Short. Every operator in the 200-char p5 original mapped straight across — and unlike p5 it
scrubs and records (pure in i, t).
// cloud-shells — a dwitter-style art-tweet reimagined in manic: ONE `cloud` of
// 10,000 points in three layers (`mod(i,3)`), placed by a polar formula — radius
// `q`, angle `c` — then coloured per point (a hue gradient per form) and bloomed
// out of the centre. Rebuilt as a textbook Short: every point a pure function of
// (i, t), so it scrubs and records.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau) — a prolific poster of
// these tiny p5.js/dwitter art formulas. This is our own hue'd, annotated take.
//
// manic examples/cloud-shells.manic
title("One formula, ten thousand points");
canvas("9:16");
template("black");
cloud(swirl, 10000, #ffffff, 0.72) {
let m = mod(i, 3) * 4; // three layers: 0, 4, 8
let k = 9 * cos(i / 81);
let e = i / 461 - 11;
let d = hypot(k, e)^4 / 40000 + 1.5 + sin(t/2 + m)/4;
let q = 89 - e*sin(k) + k*(4 + 2*sin(d*9 + e/9 - t));
let c = d + sin(t - d*4)/9 - t/9 + m;
// raw shell coords (centred at 0), then bloom + scale onto the 1080x1920 frame
let qx = q*cos(c);
let qy = (q + 30)*sin(c);
let grow = tanh(t * 0.5 + 0.12); // blooms from the centre
let x = 540 + qx * 2.9 * grow;
let y = 980 + qy * 2.9 * grow;
let r = 1.4;
// colour: a gradient along each form (index) with the three layers offset, all
// slowly cycling — every point its own hue
let hue = mod(m * 46 + i * 0.05 + t * 18, 360);
}
// ---- textbook annotations ----
caption(head, "One formula, 10,000 points", (540, 132), 40);
caption(sub, "a 200-char art-tweet, rebuilt in manic", (540, 202), 24);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\cos c,\; q\sin c)`, 44);
caption(lab, "a polar plot: radius q, angle c, per point", (540, 1784), 22);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(17);
popkorn-field
6000 points, one closed-form formula each, on a 9:16 Short. The cloud primitive places every
point by a function of its index i and live time t, blooming out of the centre (a tanh
envelope) while each point cycles its own hue — a moving field that still scrubs and records exactly.
// popkorn-field — a 6000-point parametric field for Shorts (9:16). One closed-
// form formula per point, animated by live time `t`: it blooms out of the centre
// over the first few seconds (a `tanh` growth envelope), then keeps evolving,
// while each point cycles its own hue. This is the `cloud` primitive — N points
// placed by formulas of the index `i` and the clock `t`, re-evaluated every
// frame, yet still a pure function of t (it scrubs and records exactly).
//
// The maths is domain-neutral; `cloud` knows nothing about what it draws. Note
// `hypot(a,b)`, `mod(...)` for the hue wrap, and the `0.5*(s + abs(s))` idiom for
// `max(0, s)` (min/max are reductions in manic, not 2-arg functions).
//
// manic examples/popkorn-field.manic
title("popkorn field — 6000 points, one formula each");
canvas("9:16");
template("black");
cloud(dust, 6000, #ffffff, 0.5) {
let a = 4 * cos(i / 21);
let b = i / 1880 - 20;
let k = hypot(a, b);
let S = 3 * sin(2 * a) + 0.3 / a
+ sin(i / 4465) * a * (9 + 2 * sin(b * 14 - k * 3 + 2 * t));
// raw field, centred near (200,120) at ~400px scale in the original
let fx = S + 50 * cos(k - t) + 200;
let fy = S * sin(k - t) + k * 39 - 475;
// bloom from the centre: 0.15 → ~1 over the first several seconds, so the
// full structure only resolves "after some point", then keeps drifting
let grow = tanh(t * 0.35 + 0.15);
let x = 540 + (fx - 205) * 3.3 * grow;
let y = 960 + (fy - 250) * 3.3 * grow;
// radius 1 where a*a>15 else 0.5, scaled up for the taller canvas
let s = sign(a * a - 15);
let r = (0.5 * (1 + 0.5 * (s + abs(s)))) * (2.5 + grow);
// each point its own hue, the whole wheel cycling over time
let hue = mod(i * 0.05 + t * 30, 360);
}
wait(16);
astronomical-watch
A textbook orrery from REAL SVG assets (Twemoji planets, svg() import): the Sun + six planets are
imported vector art, each turned around the Sun with inner planets faster (Kepler), over a cloud
starfield that twinkles via per-point alpha. The eight real moon-phase glyphs run along the bottom
and the orbit + Kepler maths derive on the left — real shapes, real astronomy, live.
// astronomical-watch — a textbook orrery: a clockwork solar system built from
// REAL SVG assets (Twemoji, vendored by scripts/fetch-svg-assets.sh). The Sun
// and eight planets are imported vector art; each planet is swept around the Sun by
// `turn` about the shared pivot, inner planets faster (Kepler's third law). The
// starfield is a `cloud` (per-point `alpha` twinkle), and the eight real
// moon-phase glyphs run along the bottom. The maths is derived on the left.
//
// # assets first (one-time): scripts/fetch-svg-assets.sh
// manic examples/astronomical-watch.manic
title("An orrery — a clockwork solar system");
canvas(1280, 720);
template("black");
// ---- starfield (a dense cloud; drifts, twinkles, and has depth) ----
cloud(stars, 320, #dfeaff, 1) {
let x = 1280 * noise(i, 1);
let y = mod(720 * noise(i, 5) + t * 8, 720); // slow drift down + wrap
let sz = noise(i, 9);
let r = 1.1 + sz * sz * 2.6; // 1.1..3.7 — depth, all visible
let alpha = 0.62 + 0.34 * sin(t * (1.0 + noise(i, 3)) + i * 7); // 0.28..0.96
}
// ---- orbit rings (faint, dashed), centred on the Sun at (830, 300) ----
circle(r1, (830, 300), 40); outlined(r1); dashed(r1); color(r1, #2a2a4e);
circle(r2, (830, 300), 64); outlined(r2); dashed(r2); color(r2, #2a2a4e);
circle(r3, (830, 300), 90); outlined(r3); dashed(r3); color(r3, #2a2a4e);
circle(r4, (830, 300), 118); outlined(r4); dashed(r4); color(r4, #2a2a4e);
circle(r5, (830, 300), 152); outlined(r5); dashed(r5); color(r5, #2a2a4e);
circle(r6, (830, 300), 192); outlined(r6); dashed(r6); color(r6, #2a2a4e);
circle(r7, (830, 300), 232); outlined(r7); dashed(r7); color(r7, #2a2a4e);
circle(r8, (830, 300), 270); outlined(r8); dashed(r8); color(r8, #2a2a4e);
// ---- the Sun + all eight planets (real imported SVGs) ----
svg(sun, (830, 300), "asset:svg/emoji/sun.svg", 60);
svg(mercury, (870, 300), "asset:svg/emoji/mercury.svg", 15);
svg(venus, (894, 300), "asset:svg/emoji/venus.svg", 22);
svg(earth, (920, 300), "asset:svg/emoji/earth.svg", 26);
svg(mars, (948, 300), "asset:svg/emoji/mars.svg", 19);
svg(jupiter, (982, 300), "asset:svg/emoji/jupiter.svg", 40);
svg(saturn, (1022, 300), "asset:svg/emoji/saturn.svg", 46);
svg(uranus, (1062, 300), "asset:svg/emoji/uranus.svg", 28);
svg(neptune, (1100, 300), "asset:svg/emoji/neptune.svg", 28);
// ---- the lesson (left column; text/caption CENTRE on their point) ----
caption(head, "A clockwork solar system", (240, 54), 30);
caption(sub, "inner planets orbit faster", (240, 98), 20);
hidden(head);
hidden(sub);
equation(eq1, (240, 240), `\vec p = (R\cos\omega t,\; R\sin\omega t)`, 26);
text(lab1, (240, 292), "swept around the Sun");
hidden(eq1);
hidden(lab1);
equation(eq2, (240, 410), `T^{2} \propto R^{3}`, 34);
text(lab2, (240, 466), "far = slow (Kepler)");
hidden(eq2);
hidden(lab2);
// ---- the Moon's phases: eight real glyphs along the bottom ----
caption(moonlab, "the Moon's phases", (640, 620), 22);
svg(p1, (300, 668), "asset:svg/emoji/newmoon.svg", 42);
svg(p2, (405, 668), "asset:svg/emoji/waxingcrescent.svg",42);
svg(p3, (510, 668), "asset:svg/emoji/firstquarter.svg", 42);
svg(p4, (615, 668), "asset:svg/emoji/waxinggibbous.svg", 42);
svg(p5, (720, 668), "asset:svg/emoji/fullmoon.svg", 42);
svg(p6, (825, 668), "asset:svg/emoji/waninggibbous.svg", 42);
svg(p7, (930, 668), "asset:svg/emoji/lastquarter.svg", 42);
svg(p8, (1035, 668),"asset:svg/emoji/waningcrescent.svg",42);
hidden(moonlab);
hidden(p1); hidden(p2); hidden(p3); hidden(p4);
hidden(p5); hidden(p6); hidden(p7); hidden(p8);
// ---- run it: planets orbit (in parallel), the lesson reveals alongside ----
par {
turn(mercury, (830, 300), 4320, 24, linear); // 12 revolutions — fastest
turn(venus, (830, 300), 2520, 24, linear); // 7
turn(earth, (830, 300), 1620, 24, linear); // 4.5
turn(mars, (830, 300), 1080, 24, linear); // 3
turn(jupiter, (830, 300), 432, 24, linear); // 1.2
turn(saturn, (830, 300), 252, 24, linear); // 0.7
turn(uranus, (830, 300), 162, 24, linear); // 0.45
turn(neptune, (830, 300), 108, 24, linear); // 0.3 — slowest
seq {
show(head);
wait(0.9);
show(sub);
wait(1.4);
show(eq1); show(lab1);
wait(2.4);
show(eq2); show(lab2);
wait(2.2);
show(moonlab);
show(p1); show(p2); show(p3); show(p4);
show(p5); show(p6); show(p7); show(p8);
wait(4);
}
}
manic-promo
A generative promo from the cloud primitive alone: five particle swarms fly in and assemble
into words — MANIC (a cycling rainbow) at centre, with 3B1B, Manim, Animation and Generative in
the four corners. Each is from text("…"); the mid-assembly convergence storm is the money shot.
// manic-promo — a generative promo built from the `cloud` primitive alone.
// Five particle swarms fly in and assemble into words: MANIC at the centre,
// with 3B1B, Manim, Animation and Generative claiming the four corners. Each
// word is `cloud(...) from text("…")` — the glyphs are filled with points whose
// homes arrive as `hx`/`hy`; the block re-centres and scales that home to its
// slot, then blends the swarm in from a golden-angle scatter over time `t`.
// One primitive, five words, no art assets. Change the words and it just works.
//
// manic examples/manic-promo.manic
title("manic — generative animation, from a swarm");
canvas(1080, 1080);
template("black");
// --- centre: MANIC, big, a cycling rainbow ---------------------------------
cloud(manic, 2000, #ffffff, 0.96) from text("MANIC") {
let a = 0.5 * (1 + tanh((t - mod(i * 7, 29) * 0.04 - 1.0) * 2.2));
let px = (hx - 540) * 0.62 + 540;
let py = (hy - 540) * 0.62 + 540;
let sx = 540 + cos(i * 2.39996) * (420 + mod(i * 97, 260));
let sy = 540 + sin(i * 2.39996) * (420 + mod(i * 97, 260));
let x = sx * (1 - a) + px * a;
let y = sy * (1 - a) + py * a;
let r = 2.4;
let hue = mod(hx * 0.4 + t * 22, 360);
}
// --- four corners: the world manic plays in --------------------------------
cloud(tl, 780, #3b8ee0, 0.95) from text("3B1B") {
let a = 0.5 * (1 + tanh((t - 2.4) * 2.2));
let px = (hx - 540) * 0.34 + 250;
let py = (hy - 540) * 0.34 + 240;
let x = (250 + cos(i * 2.39996) * 460) * (1 - a) + px * a;
let y = (240 + sin(i * 2.39996) * 460) * (1 - a) + py * a;
let r = 2;
}
cloud(tr, 820, #46e2c8, 0.95) from text("Manim") {
let a = 0.5 * (1 + tanh((t - 2.7) * 2.2));
let px = (hx - 540) * 0.34 + 830;
let py = (hy - 540) * 0.34 + 240;
let x = (830 + cos(i * 2.39996) * 460) * (1 - a) + px * a;
let y = (240 + sin(i * 2.39996) * 460) * (1 - a) + py * a;
let r = 2;
}
cloud(bl, 1000, #f0a54e, 0.95) from text("Animation") {
let a = 0.5 * (1 + tanh((t - 3.0) * 2.2));
let px = (hx - 540) * 0.30 + 250;
let py = (hy - 540) * 0.30 + 840;
let x = (250 + cos(i * 2.39996) * 460) * (1 - a) + px * a;
let y = (840 + sin(i * 2.39996) * 460) * (1 - a) + py * a;
let r = 2;
}
cloud(br, 1050, #b06ef0, 0.95) from text("Generative") {
let a = 0.5 * (1 + tanh((t - 3.3) * 2.2));
let px = (hx - 540) * 0.30 + 830;
let py = (hy - 540) * 0.30 + 840;
let x = (830 + cos(i * 2.39996) * 460) * (1 - a) + px * a;
let y = (840 + sin(i * 2.39996) * 460) * (1 - a) + py * a;
let r = 2;
}
wait(12);
cloud-word
Type a word, get a particle swarm: cloud(...) from text("MANIC") fills the glyphs and hands
each point its home (hx,hy); the formulas fly the swarm in from all sides, hold the word, then
burst it apart, in a left-to-right rainbow. Change the word and it just works.
// cloud-word — type a word, get a particle swarm that flies in from all sides,
// assembles into the letters, holds, then bursts apart. The `cloud` primitive's
// `from text("…")` source fills the glyphs and hands each point its home as
// `hx`/`hy`; the block formulas fly those homes in and out over time `t`. Change
// the word and it just works — the end-user's creativity, not a hard-coded logo.
//
// manic examples/cloud-word.manic
title("cloud — a word from a swarm");
canvas("9:16");
template("black");
cloud(word, 1800, #ffffff, 0.95) from text("MANIC") {
// fly-in start: golden-angle scatter around the canvas centre (540, 960)
let din = 500 + mod(i * 97, 520);
let sx = 540 + cos(i * 2.39996) * din;
let sy = 960 + sin(i * 2.39996) * din;
// burst-out end: a different hashed angle
let ex = 540 + cos(i * 4.123) * (520 + mod(i * 53, 420));
let ey = 960 + sin(i * 4.123) * (520 + mod(i * 53, 420));
// timeline: assemble ~t=1.4 (per-dot stagger), hold, burst ~t=7.5
let p = t - mod(i * 7, 29) * 0.05;
let asm = 0.5 * (1 + tanh((p - 1.4) * 2.0));
let bst = 0.5 * (1 + tanh((t - 7.5) * 1.6));
let x = sx * (1 - asm) + (hx * (1 - bst) + ex * bst) * asm;
let y = sy * (1 - asm) + (hy * (1 - bst) + ey * bst) * asm;
let r = 2.4;
// a left-to-right rainbow across the word, gently cycling
let hue = mod(hx * 0.5 + t * 25, 360);
}
wait(11);
cloud-lissajous
The SAME cloud primitive as popkorn, a completely different picture: a Lissajous curve of 600
points whose phase drifts with t so the figure folds through itself. Proof the primitive is generic.
// cloud-lissajous — the SAME `cloud` primitive as popkorn, a completely
// different picture: a Lissajous curve traced by 600 points, its horizontal
// phase drifting with time so the figure slowly folds through itself. Pure math,
// no donut in sight — proof that `cloud` is a generic point field. On a 9:16
// Short, held 20s so you can watch it evolve.
//
// manic examples/cloud-lissajous.manic
title("cloud — a breathing Lissajous figure");
canvas("9:16");
template("black");
cloud(liss, 600, #46e2c8, 0.95) {
let u = i / 600 * tau; // parameter around the curve
let x = 540 + 460 * sin(3 * u + t);
let y = 960 + 760 * sin(2 * u);
let r = 3;
// colour reveal: starts near cyan (hue 180), fans into a rotating rainbow
// around the curve as `spread` grows 0 -> 1 over the first several seconds
let spread = tanh(t * 0.3);
let hue = mod(180 + u * 57 * spread + t * 35, 360);
}
wait(25);
cloud-starfield
cloud again as a scatter, not a curve: a drifting starfield built on noise and mod — hashed
positions raining downward and wrapping, with hashed sizes.
// cloud-starfield — again the same primitive, now a scatter, not a curve: a
// drifting starfield built on `noise` and `mod`. Each star sits at a hashed
// position and rains downward, wrapping at the bottom, with a hashed size — the
// point being that `cloud` places points by whatever rule you give it. A 9:16
// Short held 20s so the drift is visible.
//
// manic examples/cloud-starfield.manic
title("cloud — a drifting starfield");
canvas("9:16");
template("black");
cloud(stars, 900, #ffffff, 0.9) {
let x = 1080 * noise(i, 1);
// fall over time and wrap the height with mod(...) (a 2-arg formula function)
let y = mod(1920 * noise(i, 7) + t * 90, 1920);
let r = 0.6 + 1.8 * noise(i, 3);
// colour reveal: stars start cool blue-white, then drift into gentle
// per-star colour as `spread` grows and the whole field slowly cycles
let spread = tanh(t * 0.25);
let hue = mod(210 + 150 * noise(i, 5) * spread + t * 18, 360);
}
wait(20);
cloud-wave-lattice
cloud as a physics-flavoured field: a 50x50 lattice unpacked from the 1-D index with floor/mod,
rippling as a travelling wave driven by t.
// cloud-wave-lattice — a 50x50 grid of points unpacked from the 1-D index with
// floor/mod, rippling as a travelling wave. Same `cloud` primitive, a physics-
// flavoured field this time: index arithmetic gives you a lattice, and `t` drives
// the wave. On a 9:16 Short, held 20s so the ripple travels through fully.
//
// manic examples/cloud-wave-lattice.manic
title("cloud — a rippling 50x50 lattice");
canvas("9:16");
template("black");
cloud(sheet, 2500, #7cf05a, 0.95) {
let col = mod(i, 50);
let row = floor(i / 50);
let x = 60 + col * 19.5;
let y = 220 + row * 28 + 70 * sin(col * 0.4 + row * 0.2 + t * 2);
let r = 3;
// colour reveal: starts near green (hue 140), then fans into rainbow bands
// running down/across the sheet as `spread` grows and the wheel cycles
let spread = tanh(t * 0.3);
let hue = mod(140 + (row * 7 + col * 3) * spread + t * 30, 360);
}
wait(20);
string-art-breath
190 straight chords (point i to point 2i) whose envelope is a cardioid caustic — an ‘eye’. It
blooms from nothing, breathes to a crimson climax, then dissolves: a differential link+turn show.
// string-art-breath — a breathing string-art caustic (the "eye"), with an arc.
//
// 190 straight chords join point i to point 2i; their envelope is a cardioid
// caustic — an "eye" — and `link` keeps each string on its two anchors. Winding an
// anchor by an amount proportional to its index (a differential turn, not a rigid
// spin) grows and shrinks the caustic. The show: the eye BLOOMS from nothing,
// BREATHES with a quickening tempo up to a CLIMAX (flushing crimson), settles, then
// DISSOLVES back to nothing — every string dead straight the whole time.
//
// manic examples/string-art-breath.manic
canvas(1000, 1000);
template("paper");
let n = 190;
let cx = 500;
let cy = 500;
let rr = 430;
let pi = 3.14159265;
for i in 0..n {
let a = i * 2 * pi / n;
dot(o{i}, (cx + rr * cos(a), cy + rr * sin(a)), 1); hidden(o{i});
dot(p{i}, (cx + rr * cos(a), cy + rr * sin(a)), 1); hidden(p{i}); // starts unwound -> blank
link(s{i}, o{i}, p{i});
color(s{i}, #101010);
stroke(s{i}, 0.5);
tag(s{i}, strings);
}
// the wordmark, waiting in the wings for the finale
text(word, (cx, cy), "manic"); size(word, 104); color(word, #101010); hidden(word);
par {
// the breath: bloom -> breathe (quickening) -> climax -> settle -> dissolve.
// every turn is on one anchor by an index-proportional amount, so the caustic
// grows and shrinks as one.
for i in 0..n {
let f = i * 360 / n; // full wind: takes point i to point 2i (the eye forms)
let d = f * 0.45; // breath depth
seq {
turn(p{i}, (cx, cy), f, 4.0, out); // BLOOM into the eye
turn(p{i}, (cx, cy), -d, 3.0, smooth); // breathe open
turn(p{i}, (cx, cy), d, 3.0, smooth); // close
turn(p{i}, (cx, cy), -d * 1.2, 2.0, smooth); // deeper, quicker
turn(p{i}, (cx, cy), d * 1.2, 2.0, smooth);
turn(p{i}, (cx, cy), -d * 1.45, 1.4, smooth); // CLIMAX
turn(p{i}, (cx, cy), d * 1.45, 1.4, smooth);
turn(p{i}, (cx, cy), -d, 3.2, smooth); // settle
turn(p{i}, (cx, cy), d, 3.2, smooth);
turn(p{i}, (cx, cy), -f, 4.5, in); // DISSOLVE back to nothing
}
}
// colour drama: an ink eye that flushes crimson through the climax, then cools,
// and finally fades right out so no wire is left for the finale.
seq {
wait(9.0);
recolor(strings, #a80028, 3.0); // blood rushes in as it quickens
recolor(strings, #101010, 6.0); // cools back to ink
wait(6.5);
fade(strings, 3.0); // every string gone by ~27.5s
}
// finale: once the wires are gone, the wordmark rises
seq {
wait(27.5);
show(word, 1.4);
recolor(word, #ff2d95, 0.8); // a brand-magenta beat
pulse(word);
wait(1.4);
}
}
wheel-radial
Sixty hollow rings breathe in a travelling wave while the whole wheel spins steady->fast->slow and the
wordmark cycles the palette — one par composing breathe + eased turn + recolor; a #hex hollow-fill trick.
// wheel-radial — a breathing radial burst that also spins with a tempo arc.
//
// 60 fixed spokes tipped with HOLLOW rings. Two things happen at once, in a
// `par { }` block:
// 1. every ring BREATHES (radius oscillates) with a phase = its position, so
// the size-wave travels around the ring (the reference-clip illusion);
// 2. the whole wheel TURNS about its centre with a steady -> fast -> slow
// tempo (three eased `turn`s), and the wordmark cycles through the palette.
// The breathing period is fixed, so the *speed* change comes from the rotation —
// `breathe` drives scale, `turn` drives position, so they compose cleanly.
//
// Rings are made truly hollow by filling them with the exact paper colour.
//
// manic examples/wheel-radial.manic
canvas(1080, 1080);
template("paper");
let n = 60;
let cx = 540;
let cy = 540;
let rin = 250; // inner radius
let len = 190; // spoke length -> outer radius = rin + len
let pi = 3.14159265;
let lobes = 5; // how many fat arcs travel around at once
// 1) the frame: spokes + hollow rings, all tagged `wheel` so `turn` spins them
for i in 0..n {
let ang = i * 2 * pi / n;
let ix = cx + rin * cos(ang);
let iy = cy + rin * sin(ang);
let ox = cx + (rin + len) * cos(ang);
let oy = cy + (rin + len) * sin(ang);
line(spoke{i}, (ix, iy), (ox, oy));
stroke(spoke{i}, 1.2);
color(spoke{i}, #3a3a4a);
tag(spoke{i}, wheel);
tag(spoke{i}, spokes);
circle(cin{i}, (ix, iy), 5);
stroke(cin{i}, 2);
color(cin{i}, #f5f2e5); // hollow: fill matches the paper background
tag(cin{i}, wheel);
circle(cout{i}, (ox, oy), 12);
stroke(cout{i}, 2.5);
color(cout{i}, #f5f2e5);
tag(cout{i}, wheel);
}
// the centre hub stays still, with the wordmark inside it
circle(hub, (cx, cy), 150);
stroke(hub, 2.5);
color(hub, #f5f2e5);
text(word, (cx, cy), "manic");
size(word, 66);
color(word, #ff2d95);
par {
// rings breathe at once, phase = position -> the wave travels
for i in 0..n {
let ph = lobes * i / n;
breathe(cin{i}, 2.4, 0.85, ph, 18);
breathe(cout{i}, 2.4, 0.92, ph, 18);
}
// the wheel spins: steady, then fast, then easing to a slow stop
seq {
turn(wheel, (cx, cy), 100, 6, linear); // steady
turn(wheel, (cx, cy), 320, 4, in); // accelerate -> fast
turn(wheel, (cx, cy), 150, 8, out); // decelerate -> slow stop
}
// and the wordmark cycles through the palette on the way
seq {
wait(6); recolor(word, #00e6ff, 1.2); recolor(spokes, #00e6ff, 1.6);
wait(4); recolor(word, #7cff6b, 1.2);
wait(3); recolor(word, #ffd166, 1.2);
}
}
wheel-square
The square sibling of wheel-radial on black — hollow SQUARES on a square. It starts DEAD STILL (the travelling breath-wave fakes rotation), then after ~6s really spins counter-clockwise; sized to stay in-frame when spun.
// wheel-square — the square sibling of wheel-radial: a breathing burst whose rings
// sit on a SQUARE, on black.
//
// The reveal: it starts DEAD STILL — only the rings breathe, and because each ring's
// phase = its position, the size-wave travels and FAKES a rotation though nothing
// moves. After ~6s the trick is dropped and the whole burst actually spins
// COUNTER-CLOCKWISE (steady -> fast -> slow), the wordmark cycling colour.
//
// Each spoke's tip is projected onto a square instead of a circle: a ray at angle
// `ang` hits a square of half-width R at distance R / max(|cos|,|sin|).
//
// manic examples/wheel-square.manic
canvas(1080, 1080);
template("mono");
let n = 64;
let cx = 540;
let cy = 540;
let rin = 195; // inner square half-width
let len = 145; // spoke length -> outer square half-width = rin + len
let pi = 3.14159265;
let lobes = 5;
// sized so the corners (at rout*sqrt(2)) stay inside the frame even when spun
// 1) the frame: spokes + hollow SQUARES on a square, tagged `wheel` so `turn` spins them
for i in 0..n {
let ang = i * 2 * pi / n;
let c = cos(ang);
let s = sin(ang);
let ca = abs(c);
let sa = abs(s);
let m = 0.5 * (ca + sa + abs(ca - sa)); // = max(|cos|,|sin|): ray -> square edge
let ix = cx + (rin / m) * c;
let iy = cy + (rin / m) * s;
let ox = cx + ((rin + len) / m) * c;
let oy = cy + ((rin + len) / m) * s;
line(spoke{i}, (ix, iy), (ox, oy));
stroke(spoke{i}, 1.2);
color(spoke{i}, #55607a);
tag(spoke{i}, wheel);
tag(spoke{i}, spokes);
rect(cin{i}, (ix, iy), 9, 9);
stroke(cin{i}, 2);
color(cin{i}, #000000); // hollow: fill matches the black background
tag(cin{i}, wheel);
rect(cout{i}, (ox, oy), 22, 22);
stroke(cout{i}, 2.5);
color(cout{i}, #000000);
tag(cout{i}, wheel);
}
// the centre hub stays still, with the wordmark inside it (a square frame too)
rect(hub, (cx, cy), 300, 300);
stroke(hub, 2.5);
color(hub, #000000);
text(word, (cx, cy), "manic");
size(word, 66);
color(word, #ff2d95);
par {
// rings breathe the WHOLE time, phase = position -> the wave travels
for i in 0..n {
let ph = lobes * i / n;
breathe(cin{i}, 2.4, 0.85, ph, 24);
breathe(cout{i}, 2.4, 0.92, ph, 24);
}
// hold still for the illusion, THEN spin COUNTER-CLOCKWISE: steady -> fast -> slow
seq {
wait(6); // just the breathing fakes rotation
turn(wheel, (cx, cy), -100, 6, linear); // now it really turns: steady
turn(wheel, (cx, cy), -320, 4, in); // accelerate -> fast
turn(wheel, (cx, cy), -150, 8, out); // decelerate -> slow stop
}
// the wordmark cycles through the palette once the spin begins
seq {
wait(6); recolor(word, #00e6ff, 1.2); recolor(spokes, #00e6ff, 1.6);
wait(5); recolor(word, #7cff6b, 1.2);
wait(4); recolor(word, #ffd166, 1.2);
}
}
wheel-duo
Four breathing bursts in a 2x2 on black with HUE’d rainbow spokes, spinning forever: circle & square
rigid (top), and circle & square counter-spinning (bottom) — the rainbow links twist into a spirograph eye.
// wheel-duo — four breathing bursts in a 2x2 grid on black, spinning forever.
//
// top row : normal spin (inner+outer together) — circle | square
// bottom row : COUNTER-spin (outer clockwise, inner counter-clockwise) — circle | square
//
// The spokes are `link`s that follow their two dots, so counter-rotation twists them
// into a spirograph "eye". Each spoke is HUE'd by its angle -> a rainbow wheel; the
// continuous rotation carries the rainbow around and never stops. Every burst
// breathes, holds still (the breath-wave fakes rotation), then keeps spinning.
//
// manic examples/wheel-duo.manic
canvas(1600, 1600);
template("black");
let n = 44;
let rin = 125;
let len = 85; // outer = rin + len = 210
let pi = 3.14159265;
let lobes = 4;
// ---- A: circle, top-left (normal spin) ----
for i in 0..n {
let ang = i * 2 * pi / n;
let ix = 440 + rin * cos(ang); let iy = 440 + rin * sin(ang);
let ox = 440 + (rin + len) * cos(ang); let oy = 440 + (rin + len) * sin(ang);
circle(a_ci{i}, (ix, iy), 5); stroke(a_ci{i}, 1.8); color(a_ci{i}, #000000); tag(a_ci{i}, a_all);
circle(a_co{i}, (ox, oy), 10); stroke(a_co{i}, 2.2); color(a_co{i}, #000000); tag(a_co{i}, a_all);
link(a_sp{i}, a_ci{i}, a_co{i}); hue(a_sp{i}, 360 * i / n); stroke(a_sp{i}, 1.3);
}
// ---- B: square, top-right (normal spin) ----
for i in 0..n {
let ang = i * 2 * pi / n;
let c = cos(ang); let s = sin(ang);
let m = 0.5 * (abs(c) + abs(s) + abs(abs(c) - abs(s))); // max(|cos|,|sin|)
let ix = 1160 + (rin / m) * c; let iy = 440 + (rin / m) * s;
let ox = 1160 + ((rin + len) / m) * c; let oy = 440 + ((rin + len) / m) * s;
rect(b_ci{i}, (ix, iy), 9, 9); stroke(b_ci{i}, 1.8); color(b_ci{i}, #000000); tag(b_ci{i}, b_all);
rect(b_co{i}, (ox, oy), 19, 19); stroke(b_co{i}, 2.2); color(b_co{i}, #000000); tag(b_co{i}, b_all);
link(b_sp{i}, b_ci{i}, b_co{i}); hue(b_sp{i}, 360 * i / n); stroke(b_sp{i}, 1.3);
}
// ---- C: circle, bottom-left (COUNTER: outer cw, inner ccw) ----
for i in 0..n {
let ang = i * 2 * pi / n;
let ix = 440 + rin * cos(ang); let iy = 1160 + rin * sin(ang);
let ox = 440 + (rin + len) * cos(ang); let oy = 1160 + (rin + len) * sin(ang);
circle(c_ci{i}, (ix, iy), 5); stroke(c_ci{i}, 1.8); color(c_ci{i}, #000000); tag(c_ci{i}, c_in);
circle(c_co{i}, (ox, oy), 10); stroke(c_co{i}, 2.2); color(c_co{i}, #000000); tag(c_co{i}, c_out);
link(c_sp{i}, c_ci{i}, c_co{i}); hue(c_sp{i}, 360 * i / n); stroke(c_sp{i}, 1.3);
}
// ---- D: square, bottom-right (COUNTER: outer cw, inner ccw) ----
for i in 0..n {
let ang = i * 2 * pi / n;
let c = cos(ang); let s = sin(ang);
let m = 0.5 * (abs(c) + abs(s) + abs(abs(c) - abs(s)));
let ix = 1160 + (rin / m) * c; let iy = 1160 + (rin / m) * s;
let ox = 1160 + ((rin + len) / m) * c; let oy = 1160 + ((rin + len) / m) * s;
rect(d_ci{i}, (ix, iy), 9, 9); stroke(d_ci{i}, 1.8); color(d_ci{i}, #000000); tag(d_ci{i}, d_in);
rect(d_co{i}, (ox, oy), 19, 19); stroke(d_co{i}, 2.2); color(d_co{i}, #000000); tag(d_co{i}, d_out);
link(d_sp{i}, d_ci{i}, d_co{i}); hue(d_sp{i}, 360 * i / n); stroke(d_sp{i}, 1.3);
}
// labels + centre title
text(title, (800, 800), "manic"); size(title, 62); color(title, #ff2d95);
text(la, (440, 720), "circle"); size(la, 26); color(la, #7f8aa3);
text(lb, (1160, 720), "square"); size(lb, 26); color(lb, #7f8aa3);
text(lc, (440, 1500), "circle counter"); size(lc, 26); color(lc, #7f8aa3);
text(ld, (1160, 1500), "square counter"); size(ld, 26); color(ld, #7f8aa3);
par {
// all four bursts breathe the whole time
for i in 0..n {
let ph = lobes * i / n;
breathe(a_ci{i}, 2.4, 0.85, ph, 38); breathe(a_co{i}, 2.4, 0.92, ph, 38);
breathe(b_ci{i}, 2.4, 0.85, ph, 38); breathe(b_co{i}, 2.4, 0.92, ph, 38);
breathe(c_ci{i}, 2.4, 0.85, ph, 38); breathe(c_co{i}, 2.4, 0.92, ph, 38);
breathe(d_ci{i}, 2.4, 0.85, ph, 38); breathe(d_co{i}, 2.4, 0.92, ph, 38);
}
// hold still, ease in, then spin CONTINUOUSLY (never stops). top: rigid; bottom: counter.
seq { wait(5); turn(a_all, (440, 440), 90, 3, in); turn(a_all, (440, 440), 1800, 30, linear); }
seq { wait(5); turn(b_all, (1160, 440), -90, 3, in); turn(b_all, (1160, 440), -1800, 30, linear); }
seq { wait(5); turn(c_out, (440, 1160), 90, 3, in); turn(c_out, (440, 1160), 1800, 30, linear); }
seq { wait(5); turn(c_in, (440, 1160), -90, 3, in); turn(c_in, (440, 1160), -1800, 30, linear); }
seq { wait(5); turn(d_out, (1160, 1160), 90, 3, in); turn(d_out, (1160, 1160), 1800, 30, linear); }
seq { wait(5); turn(d_in, (1160, 1160), -90, 3, in); turn(d_in, (1160, 1160), -1800, 30, linear); }
}
lsystem-asymptote-curves
Four canonical Asymptote rewriting systems become fitted, continuously drawable Manic paths—including a concave filled boundary and a 9,604-segment carpet curve.
// Four classic deterministic curves from the Asymptote example corpus.
// Each figure is one fitted, traceable Manic entity—even the 9,604-segment curve.
title("Four Rules, Four Infinite-Looking Curves");
canvas("16:9");
template("mono");
watermark(mark, (w*0.105, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.075), "GENERATIVE GEOMETRY · L-SYSTEMS");
text(headline, (cx, h*0.135), "A tiny rewriting rule becomes a continuous path");
text(caption, (cx, h*0.92), "One path per curve · auto-fitted · continuously drawable");
size(kicker, 20); bold(kicker); color(kicker, dim);
size(headline, 34); bold(headline);
size(caption, 20); color(caption, dim);
let left = w*0.275;
let right = w*0.725;
let upper = h*0.37;
let lower = h*0.70;
let cell = h*0.27;
lsystem(sierpinski, (left, upper), cell,
"YF", "X=YF+XF+Y;Y=XF-YF-X",
"angle=60 heading=0 iterations=7");
color(sierpinski, cyan); stroke(sierpinski, 2.5); untraced(sierpinski);
lsystem(gosper, (right, upper), cell,
"FX", "X=X+YF++YF-FX--FXFX-YF+;Y=-FX+YFYF++YF+FX--FX-Y",
"angle=60 heading=0 iterations=4");
color(gosper, magenta); stroke(gosper, 2.5); untraced(gosper);
lsystem(squareCurve, (left, lower), cell,
"F+XF+F+XF", "X=XF-F+F-XF+F+XF-F+F-X",
"angle=90 heading=45 iterations=5 closed=true fill=true");
color(squareCurve, gold); opacity(squareCurve, 0.70); stroke(squareCurve, 2.0); untraced(squareCurve);
lsystem(carpet, (right, lower), cell,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 heading=0 iterations=4");
color(carpet, lime); stroke(carpet, 2.0); untraced(carpet);
text(l1, (left, h*0.205), "SIERPINSKI CURVE · 2,187 SEGMENTS");
text(l2, (right, h*0.205), "PEANO–GOSPER · 2,401 SEGMENTS");
text(l3, (left, h*0.535), "SQUARE CURVE · 5,460 SEGMENTS");
text(l4, (right, h*0.535), "CARPET CURVE · 9,604 SEGMENTS");
size(l1, 18); size(l2, 18); size(l3, 18); size(l4, 18);
bold(l1); bold(l2); bold(l3); bold(l4);
color(l1, cyan); color(l2, magenta); color(l3, gold); color(l4, lime);
hidden(l1); hidden(l2); hidden(l3); hidden(l4);
step("one rule becomes a curve") {
par {
show(l1, 0.35);
draw(sierpinski, 1.8, smooth);
}
}
wait(0.30);
step("change the grammar") {
par {
show(l2, 0.35);
draw(gosper, 1.8, smooth);
}
}
wait(0.30);
step("close and fill the boundary") {
par {
show(l3, 0.35);
draw(squareCurve, 1.8, smooth);
}
}
wait(0.30);
step("thousands of segments stay one path") {
par {
show(l4, 0.35);
draw(carpet, 2.2, smooth);
}
}
wait(1.20);
creator-lsystem-fractal-curve
A creator Short follows one seven-segment rule from a four-edge square to a 9,604-segment space-filling curve, then closes with the Manic CTA.
// Creator story: one seven-segment rewriting rule grows from a square into a
// 9,604-segment space-filling curve. The rule is the story—not implementation.
title("How One Line Learns to Fill Space");
canvas("9:16");
template("mono");
creator(me, "@anish2good name=Manic_Geometry tagline=Rules_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Animate_your_idea safe=clean");
socials(me);
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
endcard(me, "title=Turn_Rules_Into_Stories cta=8gwifi.org/manic");
let u = (w+h-abs(w-h))/1080;
text(kicker, (cx, h*0.14), "MANIC · GENERATIVE GEOMETRY");
text(headline, (cx, h*0.24), "One rule. 9,604 lines.");
text(caption, (cx, h*0.79), "Start with a square.");
text(generation, (cx, h*0.69), "GENERATION 0 · 4 SEGMENTS");
text(rule, (cx, h*0.30), "F → FF + F + F + F + FF");
size(kicker, 20*u); bold(kicker); color(kicker, cyan);
size(headline, 30*u); bold(headline); wrap(headline, w*0.78);
size(caption, 23*u); bold(caption); wrap(caption, w*0.74);
size(generation, 20*u); bold(generation); color(generation, dim);
size(rule, 25*u); bold(rule); color(rule, gold);
let stageSize = (w+h-abs(w-h))*0.28;
let stageY = h*0.49;
lsystem(curve, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=0");
color(curve, cyan); stroke(curve, 5);
lsystem(gen1, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=1");
color(gen1, cyan); stroke(gen1, 4); hidden(gen1);
lsystem(gen2, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=2");
color(gen2, cyan); stroke(gen2, 3.5); hidden(gen2);
lsystem(gen3, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=3");
color(gen3, magenta); stroke(gen3, 3); hidden(gen3);
lsystem(finalCurve, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=4");
gradient(finalCurve, cyan, magenta, gold);
stroke(finalCurve, 2.2); untraced(finalCurve); hidden(finalCurve);
hidden(kicker); hidden(headline); hidden(rule);
step("ask the impossible question") {
seq {
par {
show(kicker, 0.35);
show(headline, 0.50);
show(rule, 0.50);
show(curve, 0.40);
}
pulse(curve, 0.60);
}
}
wait(0.45);
step("rewrite every forward move") {
seq {
say(caption, "Replace every F with seven smaller forward moves.", 0.45, smooth);
par {
become(curve, gen1, 0.85, smooth);
say(generation, "GENERATION 1 · 28 SEGMENTS", 0.35, smooth);
}
par {
become(curve, gen2, 0.95, smooth);
say(generation, "GENERATION 2 · 196 SEGMENTS", 0.35, smooth);
}
par {
become(curve, gen3, 1.05, smooth);
say(generation, "GENERATION 3 · 1,372 SEGMENTS", 0.35, smooth);
}
}
}
wait(0.50);
step("let the path fill space") {
seq {
par {
fade(curve, 0.35);
say(caption, "Repeat once more. The same rule now draws 9,604 connected segments.", 0.45, smooth);
say(generation, "GENERATION 4 · 9,604 SEGMENTS", 0.35, smooth);
}
show(finalCurve, 0.05);
draw(finalCurve, 3.20, smooth);
pulse(finalCurve, 0.80);
}
}
wait(0.75);
step("the idea is the animation") {
seq {
say(caption, "In Manic, creators describe the rule. The engine makes it move.", 0.45, smooth);
par {
recolor(headline, gold, 0.50);
pulse(finalCurve, 0.85);
}
}
}
wait(1.00);
step("creator call to action") {
par {
fade(kicker, 0.35); fade(headline, 0.35); fade(rule, 0.35);
fade(caption, 0.35); fade(generation, 0.35); fade(finalCurve, 0.45);
fade(me.footer, 0.35);
show(me.endcard, 0.60);
}
}
wait(1.80);
asymptote-tiling-reference
One two-dimensional motif becomes hex rings, a rotated grid, an outward-facing radial
system, and a nested motif-of-motifs—all through the generic repeat foundation.
// The recurring structure behind Asymptote's tiling examples:
// author one motif, then arrange it as a hex field, grid, radial ring, or a
// repeated composition. Every generated tile remains a normal Manic entity.
title("One Motif, Four Tiling Systems");
canvas("16:9");
template("mono");
watermark(mark, (w*0.11, h*0.075), "Made With Manic");
text(kicker, (cx, h*0.07), "GENERATIVE GEOMETRY · REPEAT");
text(headline, (cx, h*0.13), "Build the motif once. Compose the field.");
size(kicker, 19); bold(kicker); color(kicker, dim);
size(headline, 34); bold(headline);
let lx = w*0.27;
let rx = w*0.73;
let uy = h*0.37;
let ly = h*0.73;
// Hex rings: a small two-part diamond becomes a honeycomb field.
polygon(hexBody, (lx,uy-16), (lx+15,uy), (lx,uy+16), (lx-15,uy));
circle(hexCore, (lx,uy), 4);
color(hexBody, cyan); color(hexCore, gold);
tag(hexBody, hexMotif); tag(hexCore, hexMotif);
repeat(hexField, hexMotif, "layout=hex rings=4 spacing=30 rotate=30 scale=0.82");
hidden(hexMotif); untraced(hexField);
// Grid: a deliberately asymmetric motif proves orientation is retained.
line(gridStem, (rx-15,uy+12), (rx+14,uy-12));
circle(gridTip, (rx+14,uy-12), 5);
color(gridStem, magenta); color(gridTip, lime);
stroke(gridStem, 3);
tag(gridStem, gridMotif); tag(gridTip, gridMotif);
repeat(gridField, gridMotif, "layout=grid rows=5 cols=7 gapx=48 gapy=42 rotate=-8");
hidden(gridMotif); untraced(gridField);
// Radial: each arrow-shaped wedge faces away from the common centre.
polygon(ray, (lx,ly-22), (lx+9,ly-5), (lx,ly+4), (lx-9,ly-5));
color(ray, gold); tag(ray, rayMotif);
repeat(sun, rayMotif, "layout=radial count=18 radius=112 face=out rotate=10 scale=0.85");
hidden(rayMotif); untraced(sun);
// Nested composition: repeat a 2x2 micro-pattern as one larger radial motif.
polygon(seed, (rx-7,ly+7), (rx+7,ly+7), (rx,ly-8));
color(seed, cyan);
repeat(micro, seed, "layout=grid rows=2 cols=2 gapx=20 gapy=20 scale=0.70");
repeat(nested, micro, "layout=radial count=10 radius=105 face=out rotate=18 scale=0.82");
hidden(seed); hidden(micro); untraced(nested);
text(l1, (lx,h*0.205), "HEX RINGS · 37 MOTIFS");
text(l2, (rx,h*0.205), "ROTATED GRID · 35 MOTIFS");
text(l3, (lx,h*0.565), "RADIAL · FACE OUT");
text(l4, (rx,h*0.565), "NESTED · MOTIFS OF MOTIFS");
size(l1,17); size(l2,17); size(l3,17); size(l4,17);
bold(l1); bold(l2); bold(l3); bold(l4);
color(l1,cyan); color(l2,magenta); color(l3,gold); color(l4,lime);
step("hexagonal rings") { draw(hexField, 1.40, smooth); }
wait(0.25);
step("rectangular repetition") { draw(gridField, 1.40, smooth); }
wait(0.25);
step("radial orientation") { draw(sun, 1.20, smooth); }
wait(0.25);
step("composition remains reusable") { draw(nested, 1.60, smooth); }
wait(1.30);
creator-one-tile-pattern-story
A creator problem asks how many tiles lie in three complete hexagonal rings. Stable repeat layers, a live total, semantic colour, and LaTeX derive 1+6+12+18 = 37.
// Creator problem: count a hexagonal mosaic without counting 37 tiles one by
// one. `repeat` constructs the exact layers; counters and semantic LaTeX turn
// the geometry into a short visual proof.
title("How Many Tiles Are in Three Hexagonal Rings?");
canvas("9:16");
template("blank");
creator(me, "@anish2good name=Manic_Geometry tagline=Patterns_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Create_without_keyframes safe=clean");
socials(me);
watermark(mark, (w*0.16, h*0.055), "Made With Manic");
endcard(me, "title=Turn_Patterns_Into_Proofs cta=8gwifi.org/manic");
let u = (w+h-abs(w-h))/1080;
let boardY = h*0.43;
let tileR = 19*u;
text(kicker, (cx,h*0.105), "MANIC · VISUAL COUNTING");
text(headline, (cx,h*0.17), "Three rings surround one tile.");
text(question, (cx,h*0.235), "How many tiles are there altogether?");
text(caption, (cx,h*0.77), "Do not count one by one. Count what each ring adds.");
size(kicker, 20*u); bold(kicker); color(kicker, cyan);
size(headline, 31*u); bold(headline); wrap(headline,w*0.80);
size(question, 25*u); bold(question); wrap(question,w*0.78); color(question,gold);
size(caption, 22*u); bold(caption); wrap(caption,w*0.78); color(caption,dim);
// One regular hexagonal tile is the only authored artwork.
polygon(tile,
(cx,boardY-tileR),
(cx+0.866*tileR,boardY-0.5*tileR),
(cx+0.866*tileR,boardY+0.5*tileR),
(cx,boardY+tileR),
(cx-0.866*tileR,boardY+0.5*tileR),
(cx-0.866*tileR,boardY-0.5*tileR));
color(tile, gold); glow(tile, 0.75);
// The question silhouette: all 37 tiles, deliberately subdued.
repeat(questionField, tile,
"layout=hex rings=4 spacing=39 rotate=30 scale=0.90");
color(questionField, dim); opacity(questionField,0.30);
untraced(questionField);
// Declare largest first and smallest last. When all four groups are visible,
// the later cumulative layers cover their shared interior, leaving each newly
// added ring in its own semantic colour.
repeat(layer4, tile,
"layout=hex rings=4 spacing=39 rotate=30 scale=0.90");
color(layer4,lime); hidden(layer4); untraced(layer4);
repeat(layer3, tile,
"layout=hex rings=3 spacing=39 rotate=30 scale=0.90");
color(layer3,magenta); hidden(layer3); untraced(layer3);
repeat(layer2, tile,
"layout=hex rings=2 spacing=39 rotate=30 scale=0.90");
color(layer2,cyan); hidden(layer2); untraced(layer2);
repeat(layer1, tile,
"layout=hex rings=1 spacing=39 rotate=30 scale=0.90");
color(layer1,gold); hidden(layer1); untraced(layer1);
hidden(tile);
counter(total, (cx,h*0.655), 1, 0, "TOTAL ", " TILES");
size(total,25*u); bold(total); color(total,gold); hidden(total);
equation(work, (cx,h*0.70),
`N=\textcolor{gold}{1}+\textcolor{cyan}{6}+\textcolor{magenta}{12}+\textcolor{lime}{18}`,
31*u);
hidden(work);
hidden(kicker); hidden(headline); hidden(question); hidden(caption);
step("pose the mosaic problem") {
par {
show(kicker,0.35);
show(headline,0.50);
show(question,0.45);
show(caption,0.40);
draw(questionField,1.25,smooth);
}
}
wait(1.10);
step("focus on the construction") {
par {
fade(questionField,0.35);
say(caption,"Begin with the single centre tile.",0.40);
cam((cx,boardY),0.45,smooth);
zoom(1.10,0.45,smooth);
show(total,0.35);
}
show(layer1,0.05);
draw(layer1,0.45,smooth);
pulse(layer1,0.55);
}
wait(0.40);
step("the first ring adds six") {
par {
show(layer2,0.05);
to(total,value,7,0.55,smooth);
say(caption,"Ring 1 adds 6 tiles: one on each side.",0.40);
}
stagger(0.055) {
for i in 0..7 { draw(layer2.i{i},0.28,smooth); }
}
}
wait(0.35);
step("the second ring adds twelve") {
par {
show(layer3,0.05);
to(total,value,19,0.65,smooth);
say(caption,"Ring 2 has twice as many positions, so it adds 12.",0.45);
}
stagger(0.035) {
for i in 0..19 { draw(layer3.i{i},0.22,smooth); }
}
}
wait(0.35);
step("the third ring adds eighteen") {
par {
show(layer4,0.05);
to(total,value,37,0.75,smooth);
say(caption,"Ring 3 adds 18 more. Every new ring contributes another six.",0.45);
}
stagger(0.022) {
for i in 0..37 { draw(layer4.i{i},0.18,smooth); }
}
}
wait(0.55);
step("write what the colors counted") {
par {
show(work,0.50);
say(caption,"The colored layers give the sum directly.",0.40);
cam((cx,cy),0.45,smooth);
zoom(1.0,0.45,smooth);
}
}
wait(0.55);
step("recognize the pattern") {
rewrite(work, `N=1+6(1+2+3)`,0.80,smooth);
say(caption,"Factor out six: the ring numbers form a triangular sum.",0.45);
}
wait(0.55);
step("solve") {
par {
rewrite(work, `N=\textcolor{lime}{37}`,0.85,smooth);
say(caption,"So the mosaic contains exactly 37 tiles.",0.45);
pulse(total,0.80);
recolor(headline,gold,0.50);
}
}
wait(1.10);
step("call to action") {
par {
fade(kicker,0.30); fade(headline,0.30); fade(question,0.30);
fade(caption,0.30); fade(total,0.30); fade(work,0.30);
fade(layer1,0.35); fade(layer2,0.35); fade(layer3,0.35); fade(layer4,0.35);
fade(me.footer,0.30); show(me.endcard,0.60);
}
}
wait(1.80);
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");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
// No template call: black is the full-colour exact-black 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);
}
}
process-stream-observe
One deterministic collection journey drives two truthful views. stream progressively
moves persistent objects; observe connects the same arrival/speed measurements to a
counter and an initially empty livehistogram without callbacks or guessed keyframes.
// PROCESS FOUNDATION — the smallest complete example.
// A real persistent collection streams along a path. Both observers read the
// compiled process measurements; neither is animated with guessed values.
title("A Collection Becomes a Process");
canvas("16:9");
template("blank");
watermark(mark, (170, 58), "Made With Manic");
text(kicker, (640, 52), "MANIC · DETERMINISTIC PROCESS");
text(headline, (640, 100), "One journey. Two truthful views.");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
rect(source, (170, 300), 210, 230);
outlined(source); outline(source, dim); stroke(source, 3);
text(sourceLabel, (170, 440), "persistent collection");
size(sourceLabel, 18); color(sourceLabel, dim);
particles(packets, source, 42, 5, 17);
spline(route, (275, 300), (410, 140), (560, 470), (720, 285));
stroke(route, 4); color(route, fg); untraced(route);
livehistogram(speeds, (980, 335), 0.55, 1.05, 10, 430, 220, cyan);
text(speedLabel, (980, 190), "normalized speed");
size(speedLabel, 20); bold(speedLabel);
counter(arrivals, (640, 610), 0, 0, "arrived ", " / 42");
size(arrivals, 25); color(arrivals, dim);
text(caption, (640, 665), "stream moves real objects · observe reads the same process");
size(caption, 20); color(caption, dim);
hidden(packets); hidden(speeds); hidden(arrivals); hidden(caption);
step("introduce") {
par {
show(packets, 0.45);
draw(route, 0.65);
show(speeds, 0.45);
show(arrivals, 0.35);
show(caption, 0.35);
}
}
wait(0.35);
step("stream-and-observe") {
par {
stream(packets, route, 4.2, 34, smooth);
observe(speeds, packets, speed);
observe(arrivals, packets, arrived);
}
}
wait(1.0);
process-branching-dispatch
One source dispatches persistent requests through an authored one-of-three path network.
The destination histogram reads each request’s real seeded outcome; no service semantics
or separately timed chart animation are hidden in the engine.
// GENERIC PROCESS BRANCHING — one source, three destinations.
//
// The paths carry no service semantics. `branch` only sees a directed acyclic
// network and makes one deterministic uniform choice at every fork. The same
// foundation drives the Galton-board example.
title("Process Branching — One Source, Three Destinations");
canvas("16:9");
template("blank");
watermark(mark, (145, 70), "Made With Manic");
text(head, (cx, 72), "One collection · many truthful routes");
text(sub, (cx, 112),
"Every request keeps its identity, destination, step count, and arrival time.");
size(head, 32); bold(head);
size(sub, 19); color(sub, dim);
circle(source, (180, 340), 24);
color(source, panel); outline(source, cyan); stroke(source, 3);
particles(requests, source, 54, 4, 41);
color(requests, cyan); glow(requests, 0.7); z(requests, 8);
line(entry, (205, 340), (420, 340)); tag(entry, dispatchRoutes);
spline(upper, (420, 340), (530, 190), (680, 185)); tag(upper, dispatchRoutes);
line(middle, (420, 340), (680, 340)); tag(middle, dispatchRoutes);
spline(lower, (420, 340), (530, 490), (680, 495)); tag(lower, dispatchRoutes);
color(dispatchRoutes, dim); stroke(dispatchRoutes, 3); untraced(dispatchRoutes);
rect(worker0, (735, 185), 150, 82);
rect(worker1, (735, 340), 150, 82);
rect(worker2, (735, 495), 150, 82);
for i in 0..3 {
color(worker{i}, panel); outline(worker{i}, cyan); stroke(worker{i}, 2);
}
text(w0, (735, 185), "worker 0");
text(w1, (735, 340), "worker 1");
text(w2, (735, 495), "worker 2");
size(w0, 18); size(w1, 18); size(w2, 18);
livehistogram(destinations, (1030, 350), 0, 3, 3, 330, 300, magenta);
text(histTitle, (1030, 170), "DESTINATION OUTCOME");
size(histTitle, 19); bold(histTitle); color(histTitle, dim);
counter(arrived, (1030, 555), 0, 0, "arrived ", " / 54");
size(arrived, 21); color(arrived, dim);
text(caption, (cx, 650),
"The diagram and histogram are two views of the same seeded dispatch.");
size(caption, 21); color(caption, dim);
hidden(requests); hidden(destinations); hidden(arrived); hidden(caption);
step("network") {
par {
draw(dispatchRoutes, 0.75);
show(requests, 0.35);
show(destinations, 0.45);
show(arrived, 0.35);
show(caption, 0.35);
}
}
wait(0.35);
step("dispatch") {
par {
branch(requests, dispatchRoutes, 5.0, smooth);
observe(destinations, requests, outcome);
observe(arrived, requests, arrived);
flow(dispatchRoutes, 5.0, forward, continuous);
}
}
wait(1.0);
galton-board-process
One uncertain fork becomes eight left-or-right choices, then 180 persistent balls reveal why many more routes terminate near the center. The same real arrivals build the live bell-shaped histogram before a creator CTA closes the probability story.
// GALTON BOARD — RANDOM LOCALLY, PREDICTABLE GLOBALLY
//
// This is a probability story built from generic process vocabulary. Ordinary
// tagged lines form the board; `branch` preserves each ball through eight
// choices; `collect` and `observe` build the distribution from real arrivals.
title("How Random Choices Become a Bell Curve");
canvas("9:16");
template("blank");
watermark(mark,(w*0.16,h*0.042),"Made With Manic");
text(kicker,(cx,h*0.070),"PROBABILITY · RANDOM LOCALLY, ORDERED GLOBALLY");
text(headline,(cx,h*0.115),"Can random choices create a predictable shape?");
text(chapter,(cx,h*0.195),"1 · BEGIN WITH ONE FORK");
text(caption,(cx,h*0.855),"One ball can land almost anywhere.");
text(insight,(cx,h*0.815),"MORE ROUTES LEAD TO THE CENTER");
text(cta,(cx,h*0.930),"MAKE PROBABILITY VISIBLE → 8gwifi.org/manic");
size(kicker,18); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,31); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(chapter,19); color(chapter,cyan); bold(chapter); hidden(chapter);
size(caption,21); color(caption,dim); wrap(caption,w*0.84); hidden(caption);
size(insight,19); color(insight,lime); bold(insight); hidden(insight);
size(cta,21); color(cta,cyan); bold(cta); hidden(cta);
equation(law,(cx,h*0.158),`X\sim\operatorname{Binomial}\!\left(8,\frac12\right)`,29);
hidden(law);
let levels = 8;
let boardX = cx;
let topY = h*0.235;
let dx = w*0.065;
let dy = h*0.034;
// Every directed edge joins one row to the next. Converging endpoints create
// the ordinary rooted DAG followed by `branch`.
for r in 0..levels {
for k in 0..r+1 {
let x1 = boardX + (k-r*0.5)*dx;
let y1 = topY + r*dy;
let xl = boardX + (k-(r+1)*0.5)*dx;
let xr = boardX + (k+1-(r+1)*0.5)*dx;
let y2 = topY + (r+1)*dy;
line(left{r}_{k},(x1,y1),(xl,y2));
line(right{r}_{k},(x1,y1),(xr,y2));
tag(left{r}_{k},boardRoutes); tag(right{r}_{k},boardRoutes);
color(left{r}_{k},dim); color(right{r}_{k},dim);
stroke(left{r}_{k},1.7); stroke(right{r}_{k},1.7);
opacity(left{r}_{k},0.30); opacity(right{r}_{k},0.30);
dot(peg{r}_{k},(x1,y1),4.8);
color(peg{r}_{k},fg); glow(peg{r}_{k},0.36); tag(peg{r}_{k},pegs);
}
}
untraced(boardRoutes);
for k in 0..levels+1 {
let tx = boardX + (k-levels*0.5)*dx;
let ty = topY + levels*dy;
dot(exit{k},(tx,ty),5);
color(exit{k},gold); glow(exit{k},0.45); tag(exit{k},exits);
counter(bin{k},(tx,ty+28),k,0);
size(bin{k},15); color(bin{k},dim); tag(bin{k},exitLabels);
}
text(leftChoice,(boardX-dx*0.72,topY+dy*0.72),"LEFT");
text(rightChoice,(boardX+dx*0.72,topY+dy*0.72),"RIGHT");
size(leftChoice,15); size(rightChoice,15);
color(leftChoice,cyan); color(rightChoice,magenta);
hidden(leftChoice); hidden(rightChoice);
circle(source,(boardX,topY),13);
opacity(source,0);
particles(balls,source,180,4.0,73);
color(balls,cyan); glow(balls,0.78); z(balls,9);
let histY = h*0.695;
livehistogram(outcomes,(cx,histY),0,9,9,w*0.76,h*0.155,magenta);
text(histTitle,(cx,h*0.595),"WHERE 180 BALLS ACTUALLY LANDED");
size(histTitle,18); bold(histTitle); color(histTitle,dim);
counter(landed,(cx,h*0.785),0,0,"landed "," / 180");
size(landed,21); color(landed,dim);
hidden(pegs); hidden(exits); hidden(exitLabels);
hidden(balls); hidden(outcomes); hidden(histTitle); hidden(landed);
step("introduce one uncertain choice") {
par {
show(kicker,0.30);
show(headline,0.45);
show(law,0.45);
show(chapter,0.35);
show(caption,0.40);
draw(boardRoutes,0.90);
show(pegs,0.55);
show(exits,0.45);
show(exitLabels,0.45);
show(leftChoice,0.35);
show(rightChoice,0.35);
}
}
wait(0.60);
step("repeat the choice eight times") {
par {
show(outcomes,0.50);
show(histTitle,0.35);
show(landed,0.35);
say(chapter,"2 · REPEAT LEFT OR RIGHT EIGHT TIMES",0.38);
say(caption,"At every peg, each ball makes another equally likely left-or-right choice.",0.44);
}
}
wait(0.55);
step("let the crowd reveal the pattern") {
par {
branch(balls,boardRoutes,8.20,smooth);
collect(outcomes,balls,outcome,0.34,smooth);
observe(outcomes,balls,outcome);
observe(landed,balls,arrived);
show(balls,0.15);
seq {
say(chapter,"3 · WATCH 180 INDIVIDUAL JOURNEYS",0.38);
say(caption,"One route is unpredictable. The crowd begins to expose a stable pattern.",0.44);
wait(3.40);
say(caption,"Every bar is measured from the same balls you see falling—not animated separately.",0.44);
}
}
}
wait(0.65);
step("explain why the center wins") {
par {
pulse(outcomes.bars,0.80);
show(insight,0.45);
say(chapter,"4 · ORDER EMERGES FROM MANY CHOICES",0.38);
say(caption,"Extreme bins need nearly all-left or all-right. Many more mixed sequences end near the center.",0.48);
}
}
wait(0.75);
step("create with Manic") {
par {
pulse(outcomes.bars,0.75);
show(cta,0.45);
say(caption,"Describe the choices once. Manic keeps every route, arrival, count, and live distribution connected.",0.45);
}
}
wait(1.45);
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");
watermark(manicMark, (w*0.895-100, h*0.075+24), "Made With Manic");
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);
reactive-multiformat
One named reactive story rendered as portrait, 4:5 feed, square, or landscape with the
--canvas override. Responsive variables and layout branches reflow before construction,
while the same steps, timing, equation continuity and creator identity remain intact.
// ============================================================================
// reactive-multiformat.manic — one semantic story, four output formats
// ----------------------------------------------------------------------------
// Keep this source unchanged and render it with:
// manic examples/reactive-multiformat.manic --canvas portrait
// manic examples/reactive-multiformat.manic --canvas 4:5
// manic examples/reactive-multiformat.manic --canvas square
// manic examples/reactive-multiformat.manic --canvas 16:9
// The override is applied before w/h/cx/cy and the build-time layout branch.
// Every format keeps the same named steps, continuity, duration and identity.
// ============================================================================
title("One Story, Every Screen");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Visual_Proofs tagline=One_idea_every_screen yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Keep_the_story safe=clean");
let u = (w+h-abs(w-h)) / 2160; // min(w,h) / 1080 without a layout-specific constant
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
// Portrait Reel / Short: story reads from top to bottom.
if h > 1.45*w {
text(kicker, (cx, 125*u), "ONE SOURCE · PORTRAIT");
text(headline, (cx, 215*u), "A derivative is a world of slopes");
equation(work, (cx, 355*u), `f(x)=0.35x^2`, 54*u);
rect(stage, (cx, 900*u), 930*u, 940*u);
axes(ax, (cx, 1030*u), 410*u, 350*u, 1);
plot(curve, (cx, 1030*u), 115*u, 62*u, "0.35*x*x", (-3.5,3.5));
deriv(derivative, curve);
tangent(tan, curve, -2.7, 235*u);
slope(rate, curve, -2.7, (24*u,-30*u));
equation(curveLabel, (cx+310*u, 690*u), `f(x)`, 31*u);
equation(derivativeLabel, (cx+300*u, 1290*u), `f'(x)`, 31*u);
text(caption, (cx, 1450*u), "Move along the curve and every local slope tells part of a second story.");
wrap(caption, w*0.72);
}
// Landscape lesson: explanation on the left, visual stage on the right.
else if w > 1.25*h {
text(kicker, (w*0.25, h*0.13), "ONE SOURCE · LANDSCAPE");
text(headline, (w*0.25, h*0.24), "A derivative is a world of slopes");
equation(work, (w*0.25, h*0.38), `f(x)=0.35x^2`, 54*u);
rect(stage, (w*0.72, h*0.48), w*0.49, h*0.70);
axes(ax, (w*0.72, h*0.55), 350*u, 285*u, 1);
plot(curve, (w*0.72, h*0.55), 90*u, 48*u, "0.35*x*x", (-3.5,3.5));
deriv(derivative, curve);
tangent(tan, curve, -2.7, 190*u);
slope(rate, curve, -2.7, (20*u,-25*u));
equation(curveLabel, (w*0.88, h*0.27), `f(x)`, 29*u);
equation(derivativeLabel, (w*0.88, h*0.76), `f'(x)`, 29*u);
text(caption, (w*0.25, h*0.60), "Move along the curve and every local slope tells part of a second story.");
wrap(caption, w*0.38);
}
// Square post / 4:5 feed: compact stacked composition.
else {
text(kicker, (cx, h*0.09), "ONE SOURCE · FEED");
text(headline, (cx, h*0.17), "A derivative is a world of slopes");
equation(work, (cx, h*0.27), `f(x)=0.35x^2`, 52*u);
rect(stage, (cx, h*0.55), w*0.86, h*0.48);
axes(ax, (cx, h*0.59), 360*u, 270*u, 1);
plot(curve, (cx, h*0.59), 92*u, 48*u, "0.35*x*x", (-3.5,3.5));
deriv(derivative, curve);
tangent(tan, curve, -2.7, 195*u);
slope(rate, curve, -2.7, (20*u,-25*u));
equation(curveLabel, (cx+270*u, h*0.42), `f(x)`, 29*u);
equation(derivativeLabel, (cx+270*u, h*0.73), `f'(x)`, 29*u);
text(caption, (cx, h*0.84), "Move along the curve and every local slope tells part of a second story.");
wrap(caption, w*0.72);
}
size(kicker, 22*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 39*u); color(headline, fg); bold(headline); hidden(headline);
size(caption, 25*u); color(caption, dim); hidden(caption);
color(stage, panel); outline(stage, dim); opacity(stage, 0.72); hidden(stage);
color(ax, dim); opacity(ax, 0.52); untraced(ax);
color(curve, cyan); stroke(curve, 6*u); glow(curve, 0.75); untraced(curve);
color(derivative, magenta); stroke(derivative, 5*u); dashed(derivative, 18*u, 11*u); untraced(derivative);
color(tan, gold); stroke(tan, 4*u); hidden(tan);
color(rate, gold); hidden(rate);
color(work, cyan); hidden(work);
color(curveLabel, cyan); hidden(curveLabel);
color(derivativeLabel, magenta); hidden(derivativeLabel);
socials(me);
step("question") {
show(stage, 0.35);
show(kicker, 0.35);
show(headline, 0.45);
show(work, 0.50);
draw(ax, 0.80);
draw(curve, 1.30);
show(curveLabel, 0.40);
show(caption, 0.45);
}
wait(0.65);
step("measure") {
rewrite(work, `f'(x)=0.70x`, 0.90, smooth);
show(tan, 0.40);
show(rate, 0.40);
to(tan, x, 2.7, 2.80, smooth);
to(rate, x, 2.7, 2.80, smooth);
say(caption, "The tangent and its live slope move as one explanation.", 0.40);
}
wait(0.55);
step("collect-the-slopes") {
rewrite(work, `\textcolor{magenta}{f'(x)}=0.70x`, 0.90, smooth);
fade(tan, 0.35);
fade(rate, 0.35);
draw(derivative, 1.55);
show(derivativeLabel, 0.40);
say(caption, "Collect those local slopes and the derivative curve appears.", 0.40);
}
wait(0.65);
step("takeaway") {
rewrite(work, `\text{slope of }f=\textcolor{magenta}{f'}`, 0.95, smooth);
pulse(curve, 0.70);
pulse(derivative, 0.70);
say(caption, "The layout can change. The idea, timing and identity stay together.", 0.40);
}
wait(1.60);
parameter-journeys
One visible parameter drives a quadratic plot, its live tangent and slope, a geometric
position, scale and a derived numeric readout. Named steps animate only the value; bind
keeps every representation continuous and the source reflows across all four formats.
// ============================================================================
// parameter-journeys.manic — one value, several continuous representations
// ----------------------------------------------------------------------------
// `parameter` creates the visible control. `bind` connects it once to the
// changing plot, tangent/slope, a geometric position, scale, and p² readout.
// The named steps only animate the parameter; the visual world reacts smoothly.
// Try every format with: manic check examples/parameter-journeys.manic --canvas all
// ============================================================================
title("Parameter Journeys");
canvas("9:16");
template("mono");
creator(me, "@anish2good name=Manic_Creator_Lab tagline=One_value_many_worlds yt=zarigatongy x=@anish2good web=8gwifi.org/manic footer=none accent=cyan safe=clean");
let u = (w+h-abs(w-h)) / 2160;
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
if h > 1.45*w {
text(kicker, (cx, 120*u), "PARAMETER JOURNEY");
text(headline, (cx, 200*u), "Change one idea. Keep the world.");
equation(rule, (cx, 315*u), `y=\textcolor{cyan}{a}x^2`, 58*u);
rect(stage, (cx, 770*u), 920*u, 720*u);
axes(ax, (cx, 790*u), 400*u, 270*u, 1);
plot(curve, (cx, 790*u), 112*u, 61*u, "0.22*x*x", (-3.3,3.3));
tangent(tan, curve, 1.15, 230*u);
slope(rate, curve, 1.15, (24*u,-34*u));
parameter(a, (cx, 1215*u), -1.2, -1.5, 1.5, "a", 2);
text(positionLabel, (cx, 1365*u), "THE SAME VALUE ALSO MOVES A POINT");
line(path, (w*0.18, 1450*u), (w*0.82, 1450*u));
dot(leftEnd, (w*0.18, 1450*u), 5*u);
dot(rightEnd, (w*0.82, 1450*u), 5*u);
circle(mover, (cx, 1450*u), 22*u);
bind(a, mover, x, w*0.18, w*0.82);
counter(magnitude, (cx, 1570*u), 0, 2, "a² = ", "");
text(caption, (cx, 1690*u), "Negative, zero, positive — every connected view stays in sync.");
wrap(caption, w*0.76);
}
else if w > 1.25*h {
text(kicker, (w*0.24, h*0.12), "PARAMETER JOURNEY");
text(headline, (w*0.25, h*0.22), "Change one idea. Keep the world.");
equation(rule, (w*0.25, h*0.34), `y=\textcolor{cyan}{a}x^2`, 55*u);
parameter(a, (w*0.25, h*0.49), -1.2, -1.5, 1.5, "a", 2);
text(positionLabel, (w*0.25, h*0.64), "THE SAME VALUE MOVES A POINT");
line(path, (w*0.09, h*0.72), (w*0.41, h*0.72));
dot(leftEnd, (w*0.09, h*0.72), 5*u);
dot(rightEnd, (w*0.41, h*0.72), 5*u);
circle(mover, (w*0.25, h*0.72), 22*u);
bind(a, mover, x, w*0.09, w*0.41);
counter(magnitude, (w*0.25, h*0.82), 0, 2, "a² = ", "");
rect(stage, (w*0.72, h*0.48), w*0.49, h*0.72);
axes(ax, (w*0.72, h*0.53), 345*u, 250*u, 1);
plot(curve, (w*0.72, h*0.53), 92*u, 51*u, "0.22*x*x", (-3.3,3.3));
tangent(tan, curve, 1.15, 195*u);
slope(rate, curve, 1.15, (20*u,-28*u));
text(caption, (w*0.72, h*0.88), "Every connected view stays in sync.");
wrap(caption, w*0.42);
}
else {
text(kicker, (cx, h*0.08), "PARAMETER JOURNEY");
text(headline, (cx, h*0.15), "Change one idea. Keep the world.");
equation(rule, (cx, h*0.24), `y=\textcolor{cyan}{a}x^2`, 53*u);
rect(stage, (cx, h*0.50), w*0.86, h*0.40);
axes(ax, (cx, h*0.52), 350*u, 225*u, 1);
plot(curve, (cx, h*0.52), 94*u, 46*u, "0.22*x*x", (-3.3,3.3));
tangent(tan, curve, 1.15, 190*u);
slope(rate, curve, 1.15, (20*u,-26*u));
parameter(a, (cx, h*0.75), -1.2, -1.5, 1.5, "a", 2);
text(positionLabel, (cx, h*0.80), "ONE VALUE · PLOT · POSITION · MAGNITUDE");
line(path, (w*0.20, h*0.85), (w*0.80, h*0.85));
dot(leftEnd, (w*0.20, h*0.85), 5*u);
dot(rightEnd, (w*0.80, h*0.85), 5*u);
circle(mover, (cx, h*0.85), 20*u);
bind(a, mover, x, w*0.20, w*0.80);
counter(magnitude, (cx, h*0.875), 0, 2, "a² = ", "");
text(caption, (cx, h*0.925), "Every view stays in sync.");
wrap(caption, w*0.78);
}
// One declaration per relationship. `p` is the live parameter and `x` is the
// plot coordinate only inside a plot-formula binding.
bind(a, curve, formula, "0.22*p*x*x");
bind(a, mover, scale, "0.78+0.22*abs(p)");
bind(a, magnitude, value, "p*p");
size(kicker, 21*u); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 37*u); bold(headline); hidden(headline);
size(positionLabel, 22*u); color(positionLabel, dim); bold(positionLabel); hidden(positionLabel);
size(caption, 23*u); color(caption, dim); hidden(caption);
color(rule, cyan); hidden(rule);
color(stage, panel); outline(stage, dim); opacity(stage, 0.68); hidden(stage);
color(ax, dim); opacity(ax, 0.48); untraced(ax);
color(curve, cyan); stroke(curve, 6*u); glow(curve, 0.75); untraced(curve);
color(tan, gold); stroke(tan, 4*u); hidden(tan);
color(rate, gold); hidden(rate);
color(path, dim); stroke(path, 3*u); untraced(path);
color(leftEnd, dim); color(rightEnd, dim); hidden(leftEnd); hidden(rightEnd);
color(mover, cyan); outline(mover, fg); hidden(mover);
color(magnitude, fg); bold(magnitude); hidden(magnitude);
hidden(a.widget);
socials(me);
step("opens-down") {
show(kicker, 0.30);
show(headline, 0.40);
show(rule, 0.45);
show(stage, 0.35);
draw(ax, 0.70);
draw(curve, 1.10);
show(tan, 0.35);
show(rate, 0.35);
show(a.widget, 0.45);
show(positionLabel, 0.35);
draw(path, 0.55);
show(leftEnd, 0.25);
show(rightEnd, 0.25);
show(mover, 0.35);
show(magnitude, 0.35);
show(caption, 0.40);
}
wait(0.65);
step("flatten") {
to(a, value, 0, 2.20, smooth);
say(caption, "At zero the quadratic, tangent slope, position and magnitude settle together.", 0.40);
}
wait(0.55);
step("opens-up") {
to(a, value, 1.25, 2.40, smooth);
say(caption, "One authored parameter now carries the whole visual family upward.", 0.40);
}
wait(0.60);
step("compare") {
to(a, value, -0.65, 2.10, smooth);
say(caption, "Try another case without rebuilding the plot, diagram or readouts.", 0.40);
}
wait(0.55);
step("takeaway") {
to(a, value, 1.5, 2.30, smooth);
pulse(rule, 0.70);
pulse(mover, 0.70);
say(caption, "Declare the relationship once. Animate only what the idea means.", 0.40);
}
wait(1.60);
pascal-triangle
A non-quiz Creator v2 Short built entirely from the computation layer: each cell’s
binomial coefficient is a prod reduction, the triangle reveals row by row, the
sum-of-two-parents rule is highlighted, and colouring the odd cells uncovers Sierpinski’s
triangle — all inside a branded 9:16 shorts frame with creator identity, socials and an
end card (no grid kit needed: the triangle is triangular).
// Pascal's Triangle — a real Creator v2 Short that shows off Manic's range: the
// numbers are COMPUTED live (a `prod` reduction per cell), a hidden fractal is
// revealed, and the close reframes it as "one script — Manic does math, physics,
// algorithms & more." 9:16 shorts template, branded identity + socials + end card.
title("Pascal's Triangle");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Try_it_free safe=reels");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
let N = 8;
let cw = w*0.112;
let rh = w*0.112;
let top = h*0.21;
let rad = w*0.048;
// Build the triangle from the computation layer: each cell's binomial coefficient
// C(n,k) is a `prod` reduction on a filled disc. Tag by row (for the reveal) and
// by parity (for the fractal finale).
for n in 0..N {
for k in 0..n+1 {
let px = w*0.5 + (k - n*0.5)*cw;
let py = top + n*rh;
let val = prod(i in 1..k+1 : (n-k+i)/i);
dot(d{n}_{k}, (px, py), rad); color(d{n}_{k}, panel); tag(d{n}_{k}, discs); tag(d{n}_{k}, prow{n});
counter(v{n}_{k}, (px, py), val, 0); tag(v{n}_{k}, nums); tag(v{n}_{k}, prow{n});
let odd = round(val - 2*floor(val/2));
if odd > 0.5 { tag(d{n}_{k}, oddcells); } else { tag(d{n}_{k}, evencells); }
}
}
text(kicker, (cx, h*0.135), "This triangle of numbers hides a fractal");
size(kicker, 32); color(kicker, dim); bold(kicker); wrap(kicker, w*0.82);
text(cap, (cx, h*0.775), "every number here is computed, not typed");
size(cap, 33); color(cap, dim); wrap(cap, w*0.82);
socials(me);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
hidden(discs); hidden(nums); hidden(cap);
untraced(kicker); cursor(kicker); // opening hook types on, letter by letter
// 1 — HOOK: type the question, then build the triangle. The caption's promise —
// "computed, not typed" — is Manic's real pitch: the numbers come from a formula.
mark("hook");
type(kicker, 1.4);
show(cap, 0.4);
for n in 0..N { show(prow{n}, 0.26); wait(0.09); }
wait(0.4);
// 2 — THE RULE: 3 + 3 = 6, every cell is the sum of its two parents.
step("rule") {
seq {
say(cap, "each cell = the two above it (3 + 3 = 6)");
par { flash(d3_1, gold); flash(d3_2, gold); }
flash(d4_2, cyan);
}
}
wait(0.5);
// 3 — THE TWIST: colour the odd numbers and Sierpinski's triangle appears.
step("fractal") {
par {
say(cap, "colour the ODD numbers — and a fractal appears");
recolor(oddcells, magenta);
}
}
wait(0.9);
// 4 — THE POINT: this whole thing was one short script — and Manic's range is
// the real story. Reframe the demo as evidence of what Manic can do.
step("power") {
seq {
say(cap, "one short script did all of this");
par { pulse(discs, 0.7); recolor(cap, cyan); }
say(cap, "Manic animates math, physics, algorithms & more — from plain text");
}
}
wait(1.3);
// 5 — END CARD: the brand CTA.
mark("endcard");
par {
fade(kicker, 0.4);
fade(cap, 0.4);
fade(me.footer, 0.4);
fade(discs, 0.4);
fade(nums, 0.4);
}
show(me.endcard, 0.6);
wait(1.6);
creator-lattice-paths
The rectangular cousin of Pascal, on a real grid-kit lattice: ‘how many ways from corner
to corner moving only right and down?’ Every cell’s path-count is a prod reduction
(C(i+j,i)), the same above-plus-left rule is highlighted, the far corner holds the total,
and one actual monotone path is traced with a spline. Blueprint template, and a
different typewriter beat — the question erases and retypes itself into the answer.
// Lattice-path counting — "how many ways from corner to corner moving only right
// and down?" The SAME recurrence as Pascal (cell = the one above + the one to the
// left) but on a RECTANGLE — so it uses the grid kit. Each cell's count is a `prod`
// reduction (C(i+j,i)); afterwards one actual path is traced with `spline`.
// Different template (blueprint) + different typewriter (a question that erases and
// retypes into its answer).
title("Lattice Paths");
canvas("9:16");
template("blueprint");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=gold footer=signature cta=Try_it_free safe=reels");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
let C = 5; // columns of cells
let R = 4; // rows of cells
let s = w*0.162; // cell size
let gy = h*0.455; // grid centre y
let ox = cx - C*s*0.5;
let oy = gy - R*s*0.5;
// The rectangular lattice IS a grid-kit grid. Overlay a path-count on every cell:
// value(i,j) = C(i+j, i), the number of monotone right/down paths reaching it.
grid(lat, (cx, gy), C, R, s);
opacity(lat.cells, 0); // a lattice is its lines — clear the cell fills so the counts read
for i in 0..R {
for j in 0..C {
let val = prod(t in 1..i+1 : (j+t)/t); // C(i+j, i)
counter(pc{i}_{j}, (ox+(j+0.5)*s, oy+(i+0.5)*s), val, 0);
tag(pc{i}_{j}, pathnums); tag(pc{i}_{j}, prow{i});
}
}
// One monotone path (right/right/down/right/down/right/down) traced afterward.
spline(route,
(ox+0.5*s, oy+0.5*s), (ox+1.5*s, oy+0.5*s), (ox+2.5*s, oy+0.5*s),
(ox+2.5*s, oy+1.5*s), (ox+3.5*s, oy+1.5*s), (ox+3.5*s, oy+2.5*s),
(ox+4.5*s, oy+2.5*s), (ox+4.5*s, oy+3.5*s));
color(route, gold); stroke(route, 7); glow(route, 0.8); untraced(route); hidden(route);
// --- text: a question that later erases and retypes into the answer ---
text(kicker, (cx, h*0.115), "How many paths, corner to corner?");
size(kicker, 33); color(kicker, cyan); bold(kicker); wrap(kicker, w*0.86);
text(sub, (cx, h*0.165), "moving only RIGHT and DOWN");
size(sub, 27); color(sub, dim);
text(answer, (cx, h*0.13), "35 paths — every one of them counted.");
size(answer, 34); color(answer, gold); bold(answer); wrap(answer, w*0.86);
text(cap, (cx, h*0.80), "each cell counts the ways to reach it");
size(cap, 31); color(cap, dim); wrap(cap, w*0.84);
socials(me);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
hidden(pathnums); hidden(cap);
untraced(kicker); cursor(kicker);
untraced(sub);
untraced(answer); hidden(answer);
// 1 — HOOK: the question types on (two lines), then the lattice fills in.
mark("hook");
type(kicker, 1.2);
type(sub, 0.9);
show(cap, 0.4);
for i in 0..R { show(prow{i}, 0.3); wait(0.12); }
wait(0.4);
// 2 — THE RULE: 4 + 6 = 10 — every cell is (the one above) + (the one to the left).
step("rule") {
seq {
say(cap, "= the cell above + the cell to its left (4 + 6 = 10)");
par { flash(pc1_3, cyan); flash(pc2_2, cyan); }
flash(pc2_3, gold);
}
}
wait(0.5);
// 3 — THE ANSWER: the far corner holds the total. The question erases & retypes.
step("answer") {
seq {
par { erase(kicker, 0.5); erase(sub, 0.4); }
fade(kicker, 0.15); // clear the residual typewriter cursor left after erase
par {
pulse(pc3_4, 0.8);
flash(pc3_4, gold);
show(answer, 0.1);
type(answer, 1.1);
}
say(cap, "and every path is a right/down staircase");
}
}
wait(0.5);
// 4 — TRACE ONE: draw an actual corner-to-corner path with a spline.
step("trace") {
par {
show(route, 0.2);
draw(route, 1.6);
}
}
wait(0.7);
// 5 — THE POINT: one script did the grid, the counting AND the path.
step("power") {
seq {
say(cap, "one short script: the grid, the counting, and the path");
par { pulse(pathnums, 0.7); recolor(cap, cyan); }
say(cap, "Manic animates math, physics, algorithms & more — from plain text");
}
}
wait(1.3);
// 6 — END CARD.
mark("endcard");
par {
fade(answer, 0.4); fade(cap, 0.4); fade(me.footer, 0.4);
fade(pathnums, 0.4); fade(lat.cells, 0.4); fade(lat.lines, 0.4); fade(route, 0.4);
}
show(me.endcard, 0.6);
wait(1.6);
creator-rule90-sierpinski
A QUIZ-style Short: Rule 90 (each new cell = its two upper neighbours XOR’d) draws
Sierpinski’s triangle from a single dot — because XOR of two parents is exactly Pascal’s
triangle mod 2 (a cell is lit iff C(n,k) is odd). The gasket is the quiz’s media (fit with
figure), building as you’re asked to predict it, then the correct card and the reason
reveal. Full Creator v2 quiz: question, options, think timer, explanation and end card.
// Rule 90 → Sierpinski — a QUIZ-style Creator v2 Short. Rule 90 is the elementary
// cellular automaton where each new cell = its two upper neighbours, XOR'd. From a
// single seed it draws Sierpinski's triangle — because XOR of two parents is exactly
// Pascal's triangle mod 2 (a cell is lit iff C(n,k) is ODD). Two birds, one stone:
// the CA, the binomial parity, and the fractal are the same picture. The gasket is
// the quiz's media (fit with `figure`), building as you're asked to predict it.
title("Rule 90 → Sierpinski");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Try_it_free safe=reels");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
// The gasket: only the LIT cells (odd binomials) are drawn, so it reads as the
// clean Sierpinski triangle. `figure` fits the whole group into the media region.
let N = 16;
let cw = 30;
let rh = 30;
for n in 0..N {
for k in 0..n+1 {
let val = prod(i in 1..k+1 : (n-k+i)/i);
let odd = round(val - 2*floor(val/2));
if odd > 0.5 {
dot(sc{n}_{k}, (cx + (k - n*0.5)*cw, cy - N*rh*0.5 + n*rh), 11);
color(sc{n}_{k}, magenta); tag(sc{n}_{k}, sier); tag(sc{n}_{k}, srow{n});
}
}
}
figure(sier);
hidden(sier); // hidden at first, then builds row by row while you predict
quiz(q, "Rule 90: start with one dot. Each new cell = its two upper neighbours, XOR'd. What shape appears?",
"studio layout=media-first reveal=fade density=comfortable motion=calm safe=reels accent=cyan");
option(q, "A checkerboard");
option(q, "Sierpinski's triangle", correct);
option(q, "Random static");
option(q, "A solid pyramid");
explain(q, "XOR of two parents = Pascal's triangle mod 2 — the ODD cells are Sierpinski.", "Why");
timing(q, "calm ask=1.2 options=1.1 think=5.2 reveal=0.8 hold=2.4 stagger=0.06");
timerstyle(q, "look=ring position=below number=inside direction=drain size=medium thickness=1.1 color=cyan track=dim label=PREDICT font=mono finish=pulse");
socials(me);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
// Play the quiz; in parallel, Rule 90 builds the gasket row by row so it finishes
// right as the answer is revealed.
mark("hook");
par {
run(q);
seq {
wait(1.1);
for n in 0..N { show(srow{n}, 0.16); wait(0.26); }
}
}
mark("endcard");
par {
fade(q.parts, 0.45);
fade(sier, 0.45);
fade(me.footer, 0.45);
}
show(me.endcard, 0.6);
wait(1.6);
creator-heightmap-world
The Grid→3D bridge as a narrated Short: a grid-kit WFC map settles in 2D, then the SAME grid
rises into 3D terrain via heightmap3 — camera pull-back, orbit, and a low ridge flyover.
speak carries the story VO; say keeps short on-screen captions; Manic promo + end card close
the piece. One grid, two dimensions, inside a 9:16 creator frame.
// Your Map Is a 3D World — Manic promo Short.
// A grid settles cell by cell (Wave Function Collapse), then the SAME grid lifts
// into 3D terrain. The camera pulls back, orbits, and flies low. Story narration
// (`speak`) is separate from short on-screen captions (`say`). Outro names Manic.
title("Your Map Is a 3D World");
canvas("9:16");
template("shorts");
voice("elevenlabs"); // story narration
// voice("elevenlabs", "alice");
// voice("elevenlabs", "jessica");
// voice("cartesia");
// voice("gtts");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Try_it_free safe=reels");
watermark(mk, (w*0.955-100, h*0.045+24), "Made With Manic");
// The 2D map (grid kit) and the 3D terrain from the SAME grid.
grid(world, (cx, h*0.44), 16, 16, 52);
collapse(world, "islands", 8);
camera3((0, -1.6, 22), (0, 0, 0), 26);
heightmap3(land, world, "h*1.7 + fbm(x*1.2, y*1.2)*0.85 + 0.1", 8);
color(land, cyan);
hidden(land);
text(kicker, (cx, h*0.135), "your flat map is hiding a 3D world");
size(kicker, 33); color(kicker, cyan); bold(kicker); wrap(kicker, w*0.82);
text(cap, (cx, h*0.785), "");
size(cap, 31); color(cap, dim); wrap(cap, w*0.84);
text(promo, (cx, h*0.42), "Manic"); size(promo, 72); color(promo, cyan); bold(promo); hidden(promo);
text(promoSub, (cx, h*0.52), "plain text → animated explainers"); size(promoSub, 28); color(promoSub, magenta); wrap(promoSub, w*0.82); hidden(promoSub);
text(promoUrl, (cx, h*0.62), "8gwifi.org/manic"); size(promoUrl, 30); color(promoUrl, fg); bold(promoUrl); hidden(promoUrl);
socials(me);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
untraced(kicker); cursor(kicker);
// Narration tells the story. Captions stay short punch-lines on screen.
// speak("…") = voice only; say(cap, "…") = text only; par both when needed.
// 1 — HOOK: open the idea, then the 2D map settles (Wave Function Collapse).
mark("hook");
par {
type(kicker, 1.4);
speak("Every world starts flat. Watch a map generate itself — cell by cell — until the islands lock.");
}
step("map") {
par {
say(cap, "cell by cell…");
speak("This is Wave Function Collapse. Constraints push the grid toward a coherent coast.");
run(world, 16, 3.0);
}
}
wait(0.25);
// 2 — the same grid lifts into 3D (still near top-down, so it reads flat at first).
mark("lift");
step("height") {
par {
say(cap, "same grid → 3D");
speak("Now lift the same grid into height. Nothing was redrawn — the cells became terrain.");
fade(world.cells, 0.5); fade(world.lines, 0.5);
show(land, 0.6);
}
}
// 3 — pull back to see the WHOLE world.
mark("reveal");
step("pullback") {
par {
say(cap, "pull back");
speak("Pull the camera back. The flat map was hiding a whole landscape.");
orbit3(25, 36, 30, 3.4, smooth);
}
}
wait(0.2);
// 4 — rotate around it.
mark("rotate");
step("orbit") {
par {
say(cap, "one grid, two worlds");
speak("One grid. Two readings — a 2D map for paths, a 3D world for place. Manic bridges them.");
recolor(cap, cyan);
orbit3(185, 32, 30, 4.4, smooth);
}
}
// 5 — fly in low over the peaks.
mark("zoom");
step("fly") {
par {
say(cap, "fly the ridges");
speak("Fly in low over the ridges. Every peak is computed from the map you just watched settle.");
orbit3(255, 15, 12, 3.8, smooth);
}
}
wait(0.4);
// 6 — MANIC PROMO: this video is the product.
mark("manic");
par {
fade(kicker, 0.35); fade(cap, 0.35); fade(me.footer, 0.35); fade(land, 0.45);
}
par {
say(cap, "made with Manic");
speak("And this whole Short was generated with Manic — a language for animated explainers.");
show(promo, 0.6);
}
wait(0.2);
par {
say(cap, "plain text → motion");
speak("You write plain text. Manic turns it into grids, 3D, camera moves, and voice — no timeline editor.");
show(promoSub, 0.5);
}
wait(0.2);
par {
say(cap, "8gwifi.org/manic");
speak("Learn more, try it free, and build your own explainers at eight g wifi dot org slash manic.");
show(promoUrl, 0.5); pulse(promoUrl, 0.8);
}
wait(0.5);
// 7 — END CARD.
mark("endcard");
par {
fade(cap, 0.4); fade(promo, 0.4); fade(promoSub, 0.4); fade(promoUrl, 0.4);
}
show(me.endcard, 0.6);
wait(1.8);
creator-noise-story
How Noise Builds Worlds — a Short walking procedural noise from 1D to fractal: raw rand(x)
(jagged) vs smooth noise(x) (Perlin), then noise(x,y) tilting from a flat field into a 3D
surface, then fbm stacking octaves into fractal terrain. Every visual is one formula in the
shared expression engine — the arc that motivated adding rand/noise/fbm.
// How Noise Builds Worlds — a Creator v2 Short on procedural noise, 1D → 2D → 3D →
// fractal. Raw RANDOM values (jagged) vs smooth PERLIN noise(x); then noise(x,y) as
// a surface; then stacking octaves (fbm) for fractal detail. Every visual is one
// formula — `rand`, `noise`, `fbm`. Captions use the word-pop (karaoke) variant;
// the final scene pulls the camera back for a full view of the fractal world.
title("How Noise Builds Worlds");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Try_it_free safe=reels");
watermark(mk, (w*0.955-100, h*0.045+24), "Made With Manic");
// --- 1D: a coordinate frame with two curves, raw random vs smooth noise ---
axes(ax, (cx, h*0.33), w*0.4, 150);
color(ax, dim); untraced(ax);
plot(rnd, (cx, h*0.33), 62, 130, "rand(x)", (-6.5, 6.5));
color(rnd, magenta); untraced(rnd);
plot(perlin, (cx, h*0.33), 62, 130, "noise(x, 0)", (-6.5, 6.5));
color(perlin, cyan); glow(perlin, 0.7); untraced(perlin);
// --- 2D/3D: the same idea one dimension up. Near top-down so it reads flat first. ---
camera3((0, -1.5, 20), (0, 0, 0), 24);
surface3(hills, "noise(x*0.9, y*0.9)*2.0", (-4, 4), (-4, 4), 50);
color(hills, cyan); hidden(hills);
surface3(frac, "fbm(x*0.95, y*0.95)*2.4", (-4, 4), (-4, 4), 72);
color(frac, cyan); hidden(frac);
text(kicker, (cx, h*0.135), "random is not the same as noise");
size(kicker, 32); color(kicker, cyan); bold(kicker); wrap(kicker, w*0.82);
untraced(kicker); cursor(kicker);
// Word-pop captions — one per beat, popped in a word at a time, faded before the next.
caption(cR, "raw random values", (cx, h*0.75), 33, magenta); hidden(cR.words);
caption(cP, "smooth it into noise", (cx, h*0.75), 33, cyan); hidden(cP.words);
caption(cD, "add a dimension", (cx, h*0.75), 33, dim); hidden(cD.words);
caption(cL, "values become heights", (cx, h*0.75), 33, dim); hidden(cL.words);
caption(cF, "octaves build fractals", (cx, h*0.75), 33, cyan); hidden(cF.words);
caption(cE, "1D 2D 3D fractal", (cx, h*0.75), 34, cyan); hidden(cE.words);
caption(cM, "one formula. That's Manic.", (cx, h*0.75), 32, cyan); hidden(cM.words);
socials(me);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
// 1 — HOOK + raw random values (jagged).
mark("hook");
type(kicker, 1.3);
step("random") {
par {
draw(ax, 0.6);
draw(rnd, 1.3);
seq { wait(0.5); wordpop(cR, 0.14); }
}
}
wait(0.5);
// 2 — smooth it into Perlin noise(x).
step("perlin") {
par {
fade(cR.words, 0.3);
fade(rnd, 0.5);
draw(perlin, 1.4);
seq { wait(0.4); wordpop(cP, 0.13); }
}
}
wait(0.6);
// 3 — one dimension up: noise(x,y) as a field (top-down reads flat).
step("to2d") {
par {
fade(cP.words, 0.3);
fade(ax, 0.5); fade(perlin, 0.5);
show(hills, 0.7);
seq { wait(0.4); wordpop(cD, 0.14); }
}
}
// 4 — tilt: the field IS a 3D surface (a heightmap).
step("lift") {
par {
fade(cD.words, 0.3);
orbit3(25, 34, 16, 3.6, smooth);
seq { wait(0.5); wordpop(cL, 0.13); }
}
}
wait(0.5);
// 5 — stack octaves into fbm; pull the camera WAY back for the full world.
step("fbm") {
par {
fade(cL.words, 0.3);
fade(hills, 0.6);
show(frac, 0.7);
orbit3(120, 40, 30, 3.4, smooth);
seq { wait(0.5); wordpop(cF, 0.13); }
}
}
// 6 — a slow tour of the whole fractal world at full distance.
step("tour") {
par {
fade(cF.words, 0.3);
orbit3(240, 36, 30, 4.6, smooth);
seq { wait(0.4); wordpop(cE, 0.16); }
}
}
wait(0.4);
// 7 — THE POINT.
step("point") {
seq {
fade(cE.words, 0.3);
wordpop(cM, 0.16);
}
}
wait(1.1);
// 8 — END CARD.
mark("endcard");
par {
fade(kicker, 0.4); fade(cM.words, 0.4); fade(me.footer, 0.4); fade(frac, 0.5);
}
show(me.endcard, 0.6);
wait(1.6);
creator-free-kicks
Roberto Carlos’ impossible free kick as a narrated Short: walk-in, Magnus with values,
the RK4 freekick curls in for a GOAL while the zero-spin twin sails wide, then a cylinder
flow explains the pressure difference. speak / say carry story VO vs captions; Manic promo
outro. Physics proven, then explained, in one branded 9:16 story.
// Roberto Carlos' Banana Kick — Manic promo cut. The physics story sells the
// product: every path and streamline is computed from a plain `.manic` script,
// then the outro names Manic and points to 8gwifi.org/manic.
title("Roberto Carlos' Banana Kick");
canvas("9:16");
template("shorts");
voice("cartesia"); // story narration
// voice("elevenlabs");
// voice("gtts");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=gold footer=signature cta=Try_it_free safe=reels");
watermark(mk, (w*0.955-100, h*0.045+24), "Made With Manic");
// --- schematic top-down pitch ---
let kx = cx;
let ky = h*0.66;
rect(gbox, (cx, h*0.20), w*0.66, h*0.24); outline(gbox, dim);
rect(goal, (cx, h*0.085), w*0.34, h*0.035); outline(goal, fg);
line(gline, (cx-w*0.5, h*0.105), (cx+w*0.5, h*0.105)); color(gline, dim);
for j in 0..5 { dot(walld{j}, (cx + (j-2)*w*0.052, h*0.40), 15); color(walld{j}, dim); tag(walld{j}, wall); }
// --- Roberto Carlos: a stick figure (points + reflowing segments), gait by hand ---
let fY = ky + 48;
let hipY = ky - 4;
let neckY= ky - 96;
let handY= ky - 46;
let headY= ky - 114;
let sx0 = kx - 260;
point(neck, (sx0, neckY)); point(hip, (sx0, hipY));
point(handL,(sx0-24, handY)); point(handR,(sx0+24, handY));
point(footL,(sx0-16, fY)); point(footR,(sx0+16, fY));
hidden(neck); hidden(hip); hidden(handL); hidden(handR); hidden(footL); hidden(footR);
circle(head, (sx0, headY), 14); color(head, fg);
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, fg); color(armR, fg); color(legL, fg); color(legR, fg);
stroke(spine, 4); stroke(armL, 4); stroke(armR, 4); stroke(legL, 4); stroke(legR, 4);
tag(head, pl); tag(spine, pl); tag(armL, pl); tag(armR, pl); tag(legL, pl); tag(legR, pl);
// --- the kick: RK4 Magnus banana; the ball rides at the feet until placed ---
freekick(rc, (kx, ky), 30, 22, 3.1, 13, 175);
color(rc, gold); glow(rc, 0.8); untraced(rc); color(rc.ball, fg);
to(rc.ball, x, sx0+52, 0.01); to(rc.ball, y, ky, 0.01);
// the counterfactual: same strike, no spin → no Magnus → sails wide (red dashed)
freekick(ns, (kx, ky), 30, 22, 0, 13, 175);
color(ns, red); glow(ns, 0.4); dashed(ns); untraced(ns); hidden(ns.ball);
equation(calc, (cx, h*0.74), `\mathbf{a}_M=\kappa\,(\hat{\boldsymbol\omega}\times\mathbf{v})`, 42);
color(calc, gold); hidden(calc);
// --- Act 2 physics inset: the ball as a spinning cylinder (built now, revealed later) ---
let mpy = h*0.42;
circle(mcyl, (cx, mpy), 60); color(mcyl, gold); filled(mcyl); hidden(mcyl);
for i in 0..15 {
let y0 = -4.4 + i*0.62;
trajectory(mfl{i},
"1 - 1.44*(x*x-y*y)/((x*x+y*y)^2) - 0.7*y/(x*x+y*y)",
"0 - 2.88*x*y/((x*x+y*y)^2) + 0.7*x/(x*x+y*y)",
(-4.6, y0), (cx, mpy), 50, 460);
color(mfl{i}, cyan); glow(mfl{i}, 0.35); untraced(mfl{i}); tag(mfl{i}, flow);
}
arrow(spinT, (cx-30, mpy-72), (cx+30, mpy-72)); color(spinT, gold); hidden(spinT);
arrow(spinB, (cx+30, mpy+72), (cx-30, mpy+72)); color(spinB, gold); hidden(spinB);
arrow(force, (cx+140, mpy+30), (cx+140, mpy-70)); color(force, lime); stroke(force, 6); hidden(force);
text(flab, (cx+200, mpy-34), "Magnus\nforce"); size(flab, 24); color(flab, lime); hidden(flab);
text(fastl, (cx, mpy-115), "faster → low pressure"); size(fastl, 24); color(fastl, cyan); hidden(fastl);
text(slowl, (cx, mpy+115), "slower → high pressure"); size(slowl, 24); color(slowl, magenta); hidden(slowl);
text(kicker, (cx, h*0.13), "Roberto Carlos' impossible free kick");
size(kicker, 32); color(kicker, gold); bold(kicker); wrap(kicker, w*0.82);
text(cap, (cx, h*0.80), ""); size(cap, 30); color(cap, dim); wrap(cap, w*0.84);
text(promo, (cx, h*0.42), "Manic"); size(promo, 72); color(promo, gold); bold(promo); hidden(promo);
text(promoSub, (cx, h*0.52), "plain text → animated explainers"); size(promoSub, 28); color(promoSub, cyan); wrap(promoSub, w*0.82); hidden(promoSub);
text(promoUrl, (cx, h*0.62), "8gwifi.org/manic"); size(promoUrl, 30); color(promoUrl, fg); bold(promoUrl); hidden(promoUrl);
socials(me);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
untraced(kicker); cursor(kicker);
// Narration tells the story. Captions stay short punch-lines on screen.
// Configure once: voice("gtts"|"cartesia"). speak("…") = voice only;
// say(cap, "…") = text only; par both when needed.
// 1 — HOOK: open the legend.
mark("hook");
par {
type(kicker, 1.3);
speak("France, nineteen ninety-seven. Roberto Carlos stands thirty-five metres out — and takes a free kick the world still cannot believe.");
}
// 2 — WALK the ball in and place it (sin-phased gait).
show(cap, 0.3);
par {
say(cap, "walk it in");
speak("He walks the ball in himself and sets it down. No rush. The wall is waiting.");
}
for i in 1..11 {
let bx = sx0 + i*20; let ph = i*90; let sw = 22*sin(ph*pi/180); let lift = 8*abs(sin(ph*pi/180));
par {
move(neck,(bx, neckY-lift*0.4),0.11,smooth); move(hip,(bx, hipY),0.11,smooth);
move(handL,(bx-sw, handY),0.11,smooth); move(handR,(bx+sw, handY),0.11,smooth);
move(footL,(bx+sw, fY-lift),0.11,smooth); move(footR,(bx-sw, fY-lift),0.11,smooth);
move(head,(bx, headY-lift*0.4),0.11,smooth); move(rc.ball,(bx+52, ky),0.11,smooth);
}
}
wait(0.3);
// 3 — PACE BACK; the calculation begins.
fade(kicker, 0.4); show(calc, 0.4);
par {
say(cap, "pace back");
speak("He paces back. Eyes on the posts. Somewhere in that run-up is a curve no keeper expects.");
}
for i in 1..6 {
let bx = (kx-60) - i*18; let ph = i*90; let sw = 16*sin(ph*pi/180); let lift = 6*abs(sin(ph*pi/180));
par {
move(neck,(bx, neckY-lift*0.4),0.12,smooth); move(hip,(bx, hipY),0.12,smooth);
move(handL,(bx-sw, handY),0.12,smooth); move(handR,(bx+sw, handY),0.12,smooth);
move(footL,(bx+sw, fY-lift),0.12,smooth); move(footR,(bx-sw, fY-lift),0.12,smooth);
move(head,(bx, headY-lift*0.4),0.12,smooth);
}
}
// 4 — COMPUTE the Magnus law WITH VALUES, then lock it.
rewrite(calc, `\mathbf{a}_M=(0.05)(3.1)(30)`, 0.7, smooth);
rewrite(calc, `\mathbf{a}_M\approx 4.6\ \mathrm{m/s^2}`, 0.7, smooth);
par {
say(cap, "Magnus locks");
recolor(cap, lime);
speak("Physics names the cheat code: Magnus acceleration. Spin crossed with velocity. About four point six metres per second squared of sideways push.");
}
rewrite(calc, `\text{curl}\approx 3\ \text{m}`, 0.6, smooth);
wait(0.3);
// 5 — RAN FAST up to the ball.
recolor(cap, dim); say(cap, ""); fade(calc, 0.4);
speak("Then he runs.");
for i in 1..9 {
let bx = (kx-150) + i*15; let ph = i*120; let sw = 30*sin(ph*pi/180); let lift = 12*abs(sin(ph*pi/180));
par {
move(neck,(bx, neckY-lift*0.4),0.05,smooth); move(hip,(bx, hipY),0.05,smooth);
move(handL,(bx-sw, handY),0.05,smooth); move(handR,(bx+sw, handY),0.05,smooth);
move(footL,(bx+sw, fY-lift),0.05,smooth); move(footR,(bx-sw, fY-lift),0.05,smooth);
move(head,(bx, headY-lift*0.4),0.05,smooth);
}
}
// 6 — STRIKE → the banana swerves in by the post.
seq {
par { move(footR,(kx-4, ky+34),0.13,smooth); move(hip,(kx-30, hipY),0.13,smooth); }
par {
draw(rc, 2.0); run(rc, 2.0);
seq {
wait(1.1);
recolor(cap, gold);
par {
say(cap, "curl!");
speak("The shot starts metres wide of the post — then the air bends it in.");
}
}
}
}
wait(0.4);
// 7 — GOAL.
recolor(cap, gold);
par {
say(cap, "GOAL");
speak("Goal. Without that spin, the ball never finds the net.");
flash(goal, gold); pulse(goal, 0.7);
}
wait(0.5);
// 7.5 — proof: same strike, no spin → sails wide.
recolor(cap, red);
par {
say(cap, "no spin → wide");
speak("Same strike. Zero spin. No air pushing sideways. Watch it sail wide — the proof.");
draw(ns, 1.3);
}
wait(0.5);
// 8 — REWIND.
recolor(cap, dim);
par {
say(cap, "rewind");
speak("So why does spin bend a football? Rewind.");
erase(rc, 0.9); erase(ns, 0.9);
to(rc.ball, x, kx, 0.9, smooth); to(rc.ball, y, ky, 0.9, smooth);
}
wait(0.3);
// 9 — spinning cylinder + flow field.
par {
say(cap, "spinning cylinder");
speak("Zoom into the ball. In flight it is a spinning cylinder in the air stream.");
fade(pl, 0.5); fade(head, 0.5);
fade(gbox, 0.5); fade(goal, 0.5); fade(gline, 0.5); fade(wall, 0.5);
to(rc.ball, x, cx, 0.6, smooth); to(rc.ball, y, mpy, 0.6, smooth);
show(mcyl, 0.5);
}
stagger(0.04) { for i in 0..15 { draw(mfl{i}, 0.9); } }
wait(0.3);
// 10 — asymmetry → Magnus force.
par {
say(cap, "faster / slower");
speak("Spin drags the air: faster on top, slower below. Fast air means low pressure.");
show(spinT, 0.4); show(spinB, 0.4); show(fastl, 0.5); show(slowl, 0.5);
}
wait(0.3);
par {
say(cap, "Magnus force");
speak("The pressure difference pushes the ball sideways. That sideways push is the Magnus force.");
show(force, 0.6); show(flab, 0.5); pulse(force, 0.8);
}
wait(0.4);
// 11 — the point.
recolor(cap, cyan);
par {
say(cap, "computed, not drawn");
speak("That force is what curled Carlos' free kick. Every path you saw was computed — not drawn by hand.");
}
wait(0.5);
// 12 — MANIC PROMO: this video is the product.
mark("manic");
par {
fade(cap, 0.35); fade(me.footer, 0.35);
fade(mcyl, 0.4); fade(flow, 0.4); fade(rc.ball, 0.4);
fade(spinT, 0.4); fade(spinB, 0.4); fade(force, 0.4); fade(flab, 0.4); fade(fastl, 0.4); fade(slowl, 0.4);
fade(kicker, 0.35);
}
par {
say(cap, "made with Manic");
speak("And this whole video was generated with Manic — a language for animated explainers.");
show(promo, 0.6);
}
wait(0.25);
par {
say(cap, "plain text → motion");
speak("You write plain text. Manic turns it into motion, math, physics, and voice — no timeline editor, no keyframes by hand.");
show(promoSub, 0.5);
}
wait(0.25);
par {
say(cap, "8gwifi.org/manic");
speak("Learn more, try it free, and build your own explainers at eight g wifi dot org slash manic.");
show(promoUrl, 0.5); pulse(promoUrl, 0.8);
}
wait(0.6);
// 13 — END CARD.
mark("endcard");
par {
fade(cap, 0.4); fade(promo, 0.4); fade(promoSub, 0.4); fade(promoUrl, 0.4);
}
show(me.endcard, 0.6);
wait(1.8);
creator-pathfinding
The Shortest Path — a Short where imported VECTOR art poses the problem and a Manic
algorithm solves it: an svg() robot and pin (crisp, faithful colours) mark the start and
goal of a maze, a naive straight line cuts through the walls, then A* (gridastar) searches
the grid cell-by-cell and traces the true shortest path around them. Vectors imported, path
computed — Manic does both, in one branded 9:16 file with ElevenLabs problem→solve→CTA VO.
// The Shortest Path — a Short where VECTOR art poses the problem and a Manic
// ALGORITHM solves it. An imported robot and pin (crisp `svg()` vectors) sit at the
// start and goal of a maze; a naive straight line cuts through the walls; then A*
// (`gridastar`) searches the space cell-by-cell and traces the true shortest route.
// Vectors imported, path computed — Manic does both, in one branded 9:16 file.
title("The Shortest Path");
canvas("9:16");
template("shorts");
voice("elevenlabs"); // promo narration (ELEVENLABS_API_KEY)
// voice("elevenlabs", "alice");
// voice("cartesia"); // voice("gtts"); // free fallback
// speak("…") = voice only; say(cap, "…") = on-screen caption.
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=gold footer=signature cta=Try_it_free safe=reels");
watermark(mk, (w*0.955-100, h*0.045+24), "Made With Manic");
// --- the maze grid + the cell geometry (so the vector icons land on cells) ---
let cs = w*0.11;
let gx = cx;
let gy = h*0.43;
let sx = gx - 2.5*cs; let sy = gy - 3.5*cs; // start cell (0,0) centre
let px = gx + 2.5*cs; let py = gy + 3.5*cs; // goal cell (5,7) centre
grid(g, "@ . . . . . ; # # # # # . ; . . . . . . ; . # # # # # ; . . . . . . ; # # # # # . ; . . . . . . ; . # # # # *", (gx, gy), 6, 8, cs);
neighbors(g, "4");
// --- imported vector characters (faithful colours, crisp at any size) ---
svg(bot, (sx, sy), "asset:svg/robot.svg", cs*0.8); z(bot, 6); hidden(bot);
svg(dest, (px, py), "asset:svg/pin.svg", cs*0.8); z(dest, 6); hidden(dest);
// the naive "just go straight" line — cuts across the walls
line(naive, (sx, sy), (px, py)); color(naive, red); dashed(naive); z(naive, 4); hidden(naive);
text(hook, (cx, h*0.12), "Can the robot reach the pin?");
size(hook, 34); color(hook, gold); bold(hook); wrap(hook, w*0.82);
text(cap, (cx, h*0.78), ""); size(cap, 30); color(cap, dim); wrap(cap, w*0.84);
socials(me);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
untraced(hook); cursor(hook);
// 1 — HOOK
mark("hook");
type(hook, 1.2);
speak("How does a robot find its way through a maze?");
// 2 — BUILD: the maze is the world; the imported vectors are its characters
show(cap, 0.3); say(cap, "a maze — the robot and pin are imported SVG vectors");
speak("Drop in a robot and its goal — both imported as crisp SVG vectors.");
par { show(bot, 0.5); show(dest, 0.5); }
wait(0.6);
// 3 — PROBLEM: the straight line fails
fade(hook, 0.4); recolor(cap, red); say(cap, "a straight line? it cuts right through the walls");
speak("The obvious answer — a straight line — slams right into the walls.");
show(naive, 0.5); flash(naive, red);
wait(0.9);
// 4 — SOLVE: A* searches, then traces the real route
fade(naive, 0.4); recolor(cap, dim); say(cap, "A* searches the space, cell by cell…");
speak("So Manic runs A-star — searching the grid, cell by cell,");
gridastar(g, (0,0), (5,7), manhattan);
recolor(cap, gold); say(cap, "…and traces the true shortest path");
speak("until it traces the true shortest path around every wall.");
draw(g.path, 1.6);
wait(0.6);
// 5 — POINT
recolor(cap, cyan); say(cap, "vectors imported, path computed — Manic does both");
speak("Vectors imported, path computed — Manic does both. Turn plain text into animated explainers, free, at eight g wifi dot org slash manic.");
wait(1.2);
// 6 — END CARD — clear the maze, path, and characters so the CTA stands alone
mark("endcard");
par {
fade(cap, 0.4); fade(me.footer, 0.4);
fade(g.cells, 0.4); fade(g.lines, 0.4); fade(g.path, 0.4);
fade(g.frontier, 0.4); fade(g.visited, 0.4);
fade(bot, 0.4); fade(dest, 0.4);
}
show(me.endcard, 0.6);
wait(1.6);
creator-monty-hall
The Monty Hall Problem — the most-argued puzzle in probability, as an ElevenLabs-narrated 9:16
Short where imported SVG carries the whole story: three door emoji, a car, goats, a party
popper. You pick a door, the host lifts another to a goat — and the teaching beat is VISUAL:
the opened door’s 1/3 chance visibly SLIDES onto the other door, counting it up to 2/3, so
switching DOUBLES your odds. The payoff morphs a goat into the car with a confetti burst,
then a reveal-all finale (goat · car · goat) and a Manic CTA card. speak VO carries the
story, say the captions; every actor is one svg() line. Problem → solution → CTA.
// creator-monty-hall.manic — a Creator v2 Short on the Monty Hall problem, one of
// the most-searched (and most-argued) puzzles in probability, narrated with an
// ElevenLabs voice story. The whole cast is imported SVG — three door emoji, a car,
// goats, a party popper — and the teaching beat is visual: when the host opens a
// goat door, its 1/3 chance visibly SLIDES onto the other door, making it 2/3. The
// payoff `morph`s a goat into the car with a confetti `burst`. Problem → the
// counter-intuitive solution → CTA, 9:16 shorts template, branded.
//
// ELEVENLABS_API_KEY=... manic examples/creator-monty-hall.manic --record out.mp4
// speak("…") = voice only; say(cap,"…") = on-screen caption.
title("The Monty Hall Problem");
canvas("9:16");
template("shorts");
voice("elevenlabs");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Try_it_free safe=reels");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
// --- header ---
text(kick, (cx, h*0.10), "The Monty Hall Problem"); size(kick, 48); color(kick, fg); bold(kick); wrap(kick, w*0.86); hidden(kick);
text(sub, (cx, h*0.155), "The puzzle that fooled even the pros"); size(sub, 29); color(sub, dim); wrap(sub, w*0.82); hidden(sub);
// --- three doors (imported emoji), numbered, with a 1/3 odds chip each ---
let dy = h*0.375;
let dsz = w*0.185;
let xL = w*0.22; let xM = w*0.50; let xR = w*0.78;
svg(door1, (xL, dy), "asset:svg/emoji/door.svg", dsz); hidden(door1);
svg(door2, (xM, dy), "asset:svg/emoji/door.svg", dsz); hidden(door2);
svg(door3, (xR, dy), "asset:svg/emoji/door.svg", dsz); hidden(door3);
text(t1, (xL, dy-dsz*0.66), "1"); size(t1, 32); color(t1, dim); bold(t1); hidden(t1);
text(t2, (xM, dy-dsz*0.66), "2"); size(t2, 32); color(t2, dim); bold(t2); hidden(t2);
text(t3, (xR, dy-dsz*0.66), "3"); size(t3, 32); color(t3, dim); bold(t3); hidden(t3);
counter(chip1, (xL, dy+dsz*1.02), 33, 0, "", "%"); size(chip1, 44); color(chip1, dim); bold(chip1); hidden(chip1);
counter(chip2, (xM, dy+dsz*1.02), 33, 0, "", "%"); size(chip2, 44); color(chip2, dim); bold(chip2); hidden(chip2);
counter(chip3, (xR, dy+dsz*1.02), 33, 0, "", "%"); size(chip3, 44); color(chip3, dim); bold(chip3); hidden(chip3);
// a selection ring for "your pick"
circle(pick, (xL, dy), dsz*0.62); outlined(pick); outline(pick, cyan); stroke(pick, 5); glow(pick, 0.8); hidden(pick);
// prizes hidden BEHIND each door (revealed when a door lifts)
svg(goat1, (xL, dy), "asset:svg/emoji/goat.svg", dsz*0.86); z(goat1, -1); hidden(goat1);
svg(goat3, (xR, dy), "asset:svg/emoji/goat.svg", dsz*0.86); z(goat3, -1); hidden(goat3);
// behind door 2: a goat that MORPHS into the car (the payoff flourish)
svg(goatH, (xM, dy), "asset:svg/emoji/goat.svg", dsz*0.92); z(goatH, -1); hidden(goatH);
svg(carH, (xM, dy), "asset:svg/emoji/car.svg", dsz*1.02); z(carH, -1); hidden(carH);
morph(goatH, carH);
svg(party, (xM+dsz*0.9, dy-dsz*0.8), "asset:svg/emoji/party.svg", dsz*0.7); z(party, 6); hidden(party);
circle(bcore, (xM, dy), 30); hidden(bcore);
particles(sparks, bcore, 26, 8, 11, "random"); color(sparks, gold); glow(sparks, 0.9); z(sparks, 7); hidden(sparks);
// one caption line, retexted through the story
text(cap, (cx, h*0.66), ""); size(cap, 35); color(cap, dim); bold(cap); wrap(cap, w*0.86); hidden(cap);
socials(me);
endcard(me, "title=Always_Switch cta=8gwifi.org/manic");
// ---------------- the story (voice leads; visuals punctuate) ----------------
par { show(kick); show(sub); }
stagger(0.16) { show(door1); show(door2); show(door3); }
par { show(t1); show(t2); show(t3); show(chip1); show(chip2); show(chip3); }
show(cap, 0.3); say(cap, "1 car, 2 goats — 1/3 each.");
speak("Three doors. One car, two goats. You pick door one.");
par { show(pick); pulse(door1); }
say(cap, "You pick Door 1.");
speak("Now the host, who knows, opens door three. A goat.");
recolor(cap, dim); say(cap, "Host opens Door 3 — a goat.");
show(goat3);
par { move(door3, (xR, dy-h*0.30), 0.7, smooth); fade(door3); }
speak("The twist: door three's odds don't vanish. They slide onto door two.");
recolor(cap, cyan); say(cap, "Its 1/3 slides to Door 2.");
par { move(chip3, (xM, dy+dsz*1.02), 0.8, smooth); fade(chip3); to(chip2, value, 67, 0.8); }
par { recolor(chip2, cyan); recolor(chip1, magenta); }
speak("Most people say fifty-fifty. It isn't.");
recolor(cap, red); say(cap, "Feels like 50/50. It's not."); shake(cap);
speak("Your door stays one-in-three. Door two? Two-in-three. Switching doubles it.");
recolor(cap, gold); say(cap, "STAY 33% · SWITCH 67%");
par { pulse(chip2); pulse(door2); }
speak("So you switch...");
recolor(cap, dim); say(cap, "So you switch…");
show(goatH);
par { move(door2, (xM, dy-h*0.30), 0.7, smooth); fade(door2); fade(chip1); fade(chip2); }
speak("...and the goat becomes a car.");
to(goatH, morph, 1, 0.9, smooth);
par { fade(goatH); show(carH); }
recolor(cap, gold); say(cap, "…and win the car!");
par { show(sparks, 0.05); burst(sparks, 1.1); show(party); pulse(carH); }
wait(0.6);
speak("Door one? A goat. Switching wins two out of three.");
recolor(cap, dim); say(cap, "Door 1 was a goat.");
show(goat1);
par { move(door1, (xL, dy-h*0.30), 0.7, smooth); fade(door1); fade(pick); fade(party); }
wait(0.4);
speak("Always switch. One Manic script made this — turn any idea into a video at eight g wifi dot org slash manic.");
par { fade(kick); fade(sub); fade(cap); fade(carH); fade(goat1); fade(goat3); fade(t1); fade(t2); fade(t3); fade(chip3); }
show(me.endcard);
wait(2.6);
gradient-fastest-descent-shorts
The brachistochrone as a branded 9:16 Short: four height-colored wires race under gravity,
the cycloid wins, then a "curvature" gradient reveals why — it bends hardest at the start.
Creator chrome (socials + end card) mirrors creator-free-kicks; companion 16:9 cut is
gradient-fastest-descent.
// ============================================================================
// gradient-fastest-descent-shorts.manic — Bernoulli's brachistochrone as a
// 9:16 cinematic short, told THROUGH gradients
// (companion: gradient-fastest-descent.manic, the 16:9 cut)
// ----------------------------------------------------------------------------
// THE PROBLEM (Bernoulli, 1696): which wire gets a bead from A to B fastest?
// THE GRADIENTS: every ramp is a vertical 3-stop speedometer
// (v = √(2gΔh) — depth IS speed); the winner's secret is a curvature gradient.
//
// Pattern mirrors creator-free-kicks.manic: hook → setup → race → secret →
// endcard. Every curve and bead is RK4, not drawn.
// ============================================================================
title("The Fastest Descent");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=gold footer=signature cta=Try_it_free safe=reels");
watermark(mk, (w*0.955-100, h*0.045+24), "Made With Manic");
// ---- backdrop ----
circle(stage, (cx, h*0.42), w*0.55);
filled(stage);
gradient(stage, panel, void, radial);
opacity(stage, 0.55);
// ---- the race, sized for portrait: A upper-left, B lower-right ----
let rx = w*0.14;
let ry = h*0.30;
let ru = w*0.22;
brachistochrone(race, (rx, ry), ru);
untraced(race.straight); untraced(race.circle); untraced(race.parabola); untraced(race.cycloid);
hidden(race.bead_straight); hidden(race.bead_circle); hidden(race.bead_parabola); hidden(race.bead_cycloid);
hidden(race.markA); hidden(race.labelA); hidden(race.markB); hidden(race.labelB);
stroke(race.straight, 4); stroke(race.circle, 4); stroke(race.parabola, 4); stroke(race.cycloid, 6);
opacity(race.straight, 0.95); opacity(race.circle, 0.95); opacity(race.parabola, 0.95); opacity(race.cycloid, 1);
// depth IS speed — blue high (potential), gold low (kinetic)
gradient(race.straight, gold, cyan, blue, 270);
gradient(race.circle, gold, cyan, blue, 270);
gradient(race.parabola, gold, cyan, blue, 270);
gradient(race.cycloid, gold, cyan, blue, 270);
// ghost cycloid for the curvature reveal
brachistochrone(ghost, (rx, ry), ru);
hidden(ghost.straight); hidden(ghost.circle); hidden(ghost.parabola);
hidden(ghost.bead_straight); hidden(ghost.bead_circle); hidden(ghost.bead_parabola); hidden(ghost.bead_cycloid);
hidden(ghost.markA); hidden(ghost.labelA); hidden(ghost.markB); hidden(ghost.labelB);
hidden(ghost.cycloid); untraced(ghost.cycloid);
stroke(ghost.cycloid, 7);
gradient(ghost.cycloid, dim, magenta, "curvature");
glow(ghost.cycloid, 0.7);
// ---- typography ----
text(kicker, (cx, h*0.11), "shortest path — or fastest?");
size(kicker, 32); color(kicker, gold); bold(kicker); wrap(kicker, w*0.84);
untraced(kicker); cursor(kicker);
equation(law, (cx, h*0.62), `v=\sqrt{2g\,\Delta h}`, 38);
color(law, cyan); hidden(law);
text(cap, (cx, h*0.74), ""); size(cap, 28); color(cap, dim); wrap(cap, w*0.86);
socials(me);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
// ============================== SCRIPT ==============================
// 1 — HOOK.
mark("hook");
type(kicker, 1.2);
// 2 — THE PROBLEM.
show(cap, 0.3);
say(cap, "Bernoulli, 1696 — a bead slides from A down to B. Gravity only.");
par {
show(race.markA, 0.35); show(race.labelA, 0.25);
show(race.markB, 0.35); show(race.labelB, 0.25);
}
pulse(race.markA); pulse(race.markB);
wait(0.4);
// 3 — FOUR WIRES, each a height→speed color bar.
say(cap, "four wires — colored by depth, and depth IS speed");
stagger(0.12) {
draw(race.straight, 0.65);
draw(race.circle, 0.65);
draw(race.parabola, 0.65);
draw(race.cycloid, 0.85);
}
show(law, 0.45);
wait(0.5);
say(cap, "blue = potential · gold = all of it cashed into speed");
wait(0.7);
// 4 — THE RACE.
say(cap, "release all four at once …");
par {
show(race.bead_straight, 0.2);
show(race.bead_circle, 0.2);
show(race.bead_parabola, 0.2);
show(race.bead_cycloid, 0.2);
}
run(race, 5.5);
// 5 — THE WINNER.
recolor(cap, gold);
say(cap, "the CYCLOID wins — the straight line isn't even close");
flash(race.cycloid, magenta);
glow(race.bead_cycloid, 1.9);
pulse(race.bead_cycloid);
wait(0.7);
// 6 — THE SECRET: curvature gradient.
recolor(cap, dim);
say(cap, "its secret, colored by curvature — it bends hardest at the start");
par {
fade(race.straight, 0.4);
fade(race.circle, 0.4);
fade(race.parabola, 0.4);
fade(race.cycloid, 0.4);
fade(race.bead_straight, 0.35);
fade(race.bead_circle, 0.35);
fade(race.bead_parabola, 0.35);
fade(race.bead_cycloid, 0.35);
fade(law, 0.35);
}
show(ghost.cycloid, 0.15);
draw(ghost.cycloid, 1.3);
wait(0.35);
say(cap, "a near-vertical dive buys gold-speed immediately");
flash(ghost.cycloid, magenta);
wait(0.8);
// 7 — POINT + ENDCARD.
recolor(cap, cyan);
say(cap, "every color is computed — depth is the speedometer, the bend is the strategy");
wait(0.9);
mark("endcard");
par {
fade(cap, 0.4);
fade(me.footer, 0.4);
fade(kicker, 0.4);
fade(ghost.cycloid, 0.4);
fade(race.cycloid, 0.4);
fade(race.bead_cycloid, 0.4);
fade(race.markA, 0.3); fade(race.markB, 0.3);
fade(race.labelA, 0.3); fade(race.labelB, 0.3);
fade(stage, 0.4);
}
show(me.endcard, 0.6);
wait(1.6);
gradient-pendulum-shorts
Galileo’s chandelier as a Short told THROUGH a "speed" gradient: an RK4 pendulum swings,
then its arc is colored by true local speed — gold floods the bottom, blue pools at the tips.
The color IS the simulation (gradient(p.path, blue, cyan, gold, "speed")), not a painted
trail. Hook → reveal → energy law → prove-on-the-arc → end card.
// ============================================================================
// gradient-pendulum-shorts.manic — a 9:16 cinematic short told THROUGH a
// "speed" gradient. Sister piece to the brachistochrone pair
// (gradient-fastest-descent.manic / gradient-fastest-descent-shorts.manic).
// ----------------------------------------------------------------------------
// THE PROBLEM (Galileo, 1583): watching a chandelier swing in the Pisa
// cathedral, Galileo timed it with his own pulse. But WHERE along the arc is
// the bob fastest — and how would you ever see that?
//
// THE GRADIENT DOES THE PHYSICS: the swing arc is a pre-simulated RK4
// trajectory, uniformly sampled in time — so gradient(p.path, ..., "speed")
// colors it by its TRUE local speed. Gold floods the bottom (all kinetic),
// blue pools at the ends (the bob stands still for an instant). Nothing is
// keyframed; the color IS the simulation.
//
// Pattern mirrors creator-free-kicks.manic: hook → problem → reveal → law →
// prove on the painted arc → endcard.
// ============================================================================
title("Galileo's Chandelier");
canvas("9:16");
template("shorts");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_math_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=gold footer=signature cta=Try_it_free safe=reels");
watermark(mk, (w*0.955-100, h*0.045+24), "Made With Manic");
// ---- backdrop: a radial pool of light under the chandelier ----
circle(halo, (cx, h*0.42), w*0.52);
filled(halo);
gradient(halo, panel, void, radial);
opacity(halo, 0.8);
// ---- the chandelier: a real RK4 pendulum, arc uniformly sampled in time ----
pendulum(p, (cx, h*0.26), 1, 55, w*0.36);
hidden(p.overlays);
untraced(p.path);
stroke(p.path, 7);
opacity(p.path, 0.95);
// the reveal: 3 stops by TRUE local speed — slowest first, fastest last
gradient(p.path, blue, cyan, gold, "speed");
color(p.bob, gold); glow(p.bob, 0.7);
stroke(p.rod, 3);
// ---- typography ----
text(kicker, (cx, h*0.11), "where is a pendulum FASTEST?");
size(kicker, 34); color(kicker, gold); bold(kicker); wrap(kicker, w*0.82);
untraced(kicker); cursor(kicker);
equation(law, (cx, h*0.64), `mgh \;=\; \tfrac{1}{2}mv^2`, 40);
color(law, cyan); hidden(law);
text(cap, (cx, h*0.76), ""); size(cap, 30); color(cap, dim); wrap(cap, w*0.84);
socials(me);
endcard(me, "title=Try_Manic cta=8gwifi.org/manic");
// ============================== SCRIPT ==============================
// 1 — HOOK.
mark("hook");
type(kicker, 1.2);
// 2 — GALILEO'S PROBLEM.
show(cap, 0.3);
say(cap, "1583 — Galileo times a swinging chandelier with his own pulse");
par {
show(p.pivot, 0.4);
show(p.rod, 0.4);
show(p.bob, 0.4);
}
pulse(p.bob);
wait(0.4);
// 3 — WATCH IT SWING. Your eye can't catch where it's quickest.
say(cap, "watch it — can you SEE where it moves fastest?");
swing(p, 4);
wait(0.3);
// 4 — THE REVEAL: paint the arc with its own speed.
say(cap, "freeze — and color the arc by its true speed");
draw(p.path, 1.8);
wait(0.5);
recolor(cap, gold);
say(cap, "gold floods the bottom — blue pools at the tips, where it stops for an instant");
flash(p.path, gold);
wait(0.8);
// 5 — WHY: energy. Height traded for speed, every swing, exactly.
recolor(cap, dim);
say(cap, "height falls, speed rises — energy just changes clothes");
show(law, 0.5);
wait(0.5);
rewrite(law, `v=\sqrt{2gh}`, 0.8, smooth);
say(cap, "at the lowest point ALL the height has become speed — that's the gold");
wait(0.8);
// 6 — PROVE IT: run the bob on its own painted speedometer.
recolor(cap, cyan);
say(cap, "the color is computed, not painted — a real RK4 swing on its own speedometer");
swing(p, 4);
wait(0.5);
// 7 — END CARD.
mark("endcard");
par {
fade(cap, 0.4);
fade(me.footer, 0.4);
fade(law, 0.4);
fade(kicker, 0.4);
fade(p.path, 0.4);
fade(p.rod, 0.4);
fade(p.bob, 0.4);
fade(p.pivot, 0.4);
fade(halo, 0.4);
}
show(me.endcard, 0.6);
wait(1.6);
creator-why-rockets-use-stages
Why rockets discard their own hardware, told as a vertical Creator Short. Three reusable physics SVG stages lift together, separate independently, and trigger three continuous velocity-curve segments. The curve never jumps at separation—the changing slope explains the real advantage: later engines accelerate less dead mass. Story → science → Manic CTA.
// WHY ROCKETS USE STAGES — a 9:16 creator short built from three independently
// movable physics SVGs. The velocity curve stays continuous at separation:
// dropping an empty stage changes the following acceleration, not past speed.
title("Why Do Rockets Use Stages?");
canvas("9:16");
template("shorts");
voice("gtts");
creator(me, "@anish2good name=Manic_Physics tagline=Physics_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=gold footer=signature cta=Try_it_free safe=reels");
socials(me);
watermark(mark, (w*0.82, h*0.91), "Made With Manic");
endcard(me, "title=Build_Physics_Stories cta=8gwifi.org/manic");
// ---- headline and story caption ----
text(kicker, (340, 122), "ROCKET SCIENCE · IN 30 SECONDS");
text(headline, (340, 184), "Why throw parts of a rocket away?");
text(caption, (cx, 1035), "At liftoff, every engine must accelerate the entire rocket.");
size(kicker, 21); bold(kicker); color(kicker, cyan); hidden(kicker);
size(headline, 38); bold(headline); wrap(headline, 590); hidden(headline);
size(caption, 25); color(caption, dim); wrap(caption, 850); hidden(caption);
sticky(kicker); sticky(headline); sticky(caption);
sticky(mark); sticky(me.footer); sticky(me.endcard);
// ---- one rocket, authored as three persistent SVG stages ----
let rx = 805;
svg(upper, (rx, 420), "asset:svg/physics/rocket-upper-stage.svg", 160);
svg(second, (rx, 570), "asset:svg/physics/rocket-second-stage.svg", 145);
svg(booster, (rx, 730), "asset:svg/physics/rocket-first-stage.svg", 155);
hidden(upper); hidden(second); hidden(booster);
z(upper, 8); z(second, 7); z(booster, 6);
polygon(flame1, (rx-30, 820), (rx, 925), (rx+30, 820));
gradient(flame1, gold, magenta, 90); filled(flame1); glow(flame1, 1.1); hidden(flame1);
polygon(flame2, (rx-23, 660), (rx, 740), (rx+23, 660));
gradient(flame2, cyan, magenta, 90); filled(flame2); glow(flame2, 0.9); hidden(flame2);
polygon(flame3, (rx-19, 520), (rx, 585), (rx+19, 520));
gradient(flame3, gold, cyan, 90); filled(flame3); glow(flame3, 0.9); hidden(flame3);
// ---- a truthful, continuous velocity story ----
let gx = 170;
let gy = 1510;
let sx = 120;
let sy = 72;
line(xaxis, (gx, gy), (930, gy)); color(xaxis, dim); stroke(xaxis, 3);
line(yaxis, (gx, gy), (gx, 1180)); color(yaxis, dim); stroke(yaxis, 3);
text(vlabel, (125, 1175), "velocity"); size(vlabel, 20); color(vlabel, dim);
text(tlabel, (940, 1542), "time"); size(tlabel, 20); color(tlabel, dim);
text(chartTitle, (cx, 1135), "VELOCITY KEEPS BUILDING");
size(chartTitle, 22); bold(chartTitle); color(chartTitle, fg);
plot(v1, (gx, gy), sx, sy, "0.18*x*x", (0,2));
plot(v2, (gx, gy), sx, sy, "0.72 + 0.36*(x-2) + 0.16*(x-2)*(x-2)", (2,4));
plot(v3, (gx, gy), sx, sy, "2.08 + 0.50*(x-4) + 0.20*(x-4)*(x-4)", (4,6));
color(v1, gold); color(v2, magenta); color(v3, cyan);
stroke(v1, 7); stroke(v2, 7); stroke(v3, 7);
untraced(v1); untraced(v2); untraced(v3);
line(sep1, (gx+2*sx, gy), (gx+2*sx, 1215)); dashed(sep1, 10, 9); color(sep1, dim);
line(sep2, (gx+4*sx, gy), (gx+4*sx, 1215)); dashed(sep2, 10, 9); color(sep2, dim);
text(sep1label, (gx+2*sx+55, 1240), "STAGE 1");
text(sep2label, (gx+4*sx+55, 1240), "STAGE 2");
size(sep1label, 18); size(sep2label, 18);
color(sep1label, gold); color(sep2label, magenta);
dot(join1, (gx+2*sx, gy-0.72*sy), 8); color(join1, gold);
dot(join2, (gx+4*sx, gy-2.08*sy), 8); color(join2, magenta);
hidden(xaxis); hidden(yaxis); hidden(vlabel); hidden(tlabel); hidden(chartTitle);
hidden(sep1); hidden(sep2); hidden(sep1label); hidden(sep2label);
hidden(join1); hidden(join2);
// The chart is a screen-space instrument. Above it, the active vehicle keeps
// climbing during every narration and separation beat.
tag(xaxis, flightHud); tag(yaxis, flightHud);
tag(vlabel, flightHud); tag(tlabel, flightHud); tag(chartTitle, flightHud);
tag(v1, flightHud); tag(v2, flightHud); tag(v3, flightHud);
tag(sep1, flightHud); tag(sep2, flightHud);
tag(sep1label, flightHud); tag(sep2label, flightHud);
tag(join1, flightHud); tag(join2, flightHud);
sticky(flightHud);
// ============================== STORY ==============================
step("hook") {
par {
show(kicker, 0.35);
show(headline, 0.50);
show(caption, 0.35);
stagger(0.10) {
show(upper, 0.35);
show(second, 0.35);
show(booster, 0.35);
}
speak("Why does a rocket throw away parts while it is still flying?");
}
}
wait(0.25);
step("first-stage") {
par {
show(xaxis, 0.30); show(yaxis, 0.30);
show(vlabel, 0.25); show(tlabel, 0.25); show(chartTitle, 0.30);
show(flame1, 0.20);
draw(v1, 4.0);
shift(upper, (0,-80), 4.0, smooth);
shift(second, (0,-80), 4.0, smooth);
shift(booster, (0,-80), 4.0, smooth);
shift(flame1, (0,-80), 4.0, smooth);
shift(flame2, (0,-80), 4.0, smooth);
shift(flame3, (0,-80), 4.0, smooth);
say(caption, "Stage 1 burns hard—but it also carries tanks that will soon be empty.", 0.40);
speak("Stage one must push every tank and engine above it.");
}
}
step("drop-empty-mass") {
par {
show(sep1, 0.25); show(sep1label, 0.25); show(join1, 0.20);
fade(flame1, 0.18);
shift(booster, (-120,260), 1.05, in);
rotate(booster, -18, 1.05, smooth);
seq { wait(0.62); fade(booster, 0.35); }
shift(upper, (0,-45), 3.6, smooth);
shift(second, (0,-45), 3.6, smooth);
shift(flame2, (0,-45), 3.6, smooth);
shift(flame3, (0,-45), 3.6, smooth);
say(caption, "Separation does not add speed. It removes dead mass.", 0.35);
speak("Separation adds no speed. It only removes empty mass.");
}
}
cue(whoosh);
step("second-stage") {
par {
show(flame2, 0.18);
draw(v2, 4.0);
shift(upper, (0,-70), 4.0, smooth);
shift(second, (0,-70), 4.0, smooth);
shift(flame2, (0,-70), 4.0, smooth);
shift(flame3, (0,-70), 4.0, smooth);
say(caption, "The lighter vehicle gains more velocity from its next engine.", 0.35);
speak("The next engine pushes less mass, so velocity builds faster.");
}
}
step("third-stage") {
par {
show(sep2, 0.25); show(sep2label, 0.25); show(join2, 0.20);
fade(flame2, 0.18);
shift(second, (125,235), 1.0, in);
rotate(second, 16, 1.0, smooth);
seq { wait(0.58); fade(second, 0.35); }
seq {
wait(0.28);
show(flame3, 0.18);
par {
draw(v3, 3.25);
shift(upper, (0,-50), 3.25, smooth);
shift(flame3, (0,-50), 3.25, smooth);
}
}
say(caption, "Each stage discards structure, then the next stage keeps accelerating.", 0.40);
speak("It repeats: less dead weight, more useful acceleration.");
}
}
cue(chime);
step("answer") {
par {
fade(kicker, 0.30);
pulse(upper, 0.65);
flash(v3, cyan);
say(headline, "Stages turn fuel into velocity—not into dead weight.", 0.45);
say(caption, "The curve never jumps. Its steeper climb is the advantage.", 0.40);
speak("Staging does not teleport a rocket faster. It stops wasting thrust on empty tanks.");
shift(upper, (0,-55), 6.4, smooth);
shift(flame3, (0,-55), 6.4, smooth);
}
}
step("creator-cta") {
par {
fade(kicker, 0.30); fade(headline, 0.30); fade(caption, 0.30);
fade(upper, 0.30); fade(flame3, 0.30);
fade(xaxis, 0.30); fade(yaxis, 0.30);
fade(vlabel, 0.30); fade(tlabel, 0.30); fade(chartTitle, 0.30);
fade(v1, 0.30); fade(v2, 0.30); fade(v3, 0.30);
fade(sep1, 0.30); fade(sep2, 0.30);
fade(sep1label, 0.30); fade(sep2label, 0.30);
fade(join1, 0.30); fade(join2, 0.30);
fade(me.footer, 0.30);
fade(mark, 0.30);
}
show(me.endcard, 0.60);
}
wait(1.6);
creator-how-rocket-launch-works
How a rocket launches, told entirely through a reusable cutaway SVG and Manic motion. The story reveals payload, fuel, oxidizer, combustion chamber, and nozzle; then hot gas streams downward while the vehicle moves upward. It closes the common misconception directly: rockets work in space because they carry their own oxidizer. Cutaway → reaction → thrust → CTA.
// HOW A ROCKET LAUNCH WORKS — a 9:16 cutaway story.
// The reusable SVG supplies the vehicle; Manic supplies the teaching:
// callouts, chamber reaction, directional exhaust, thrust, motion, and CTA.
title("How a Rocket Launch Works");
canvas("9:16");
template("shorts");
voice("gtts");
creator(me, "@anish2good name=Manic_Physics tagline=Physics_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=gold footer=signature cta=Try_it_free safe=reels");
socials(me);
watermark(mark, (w*0.82, h*0.91), "Made With Manic");
endcard(me, "title=Make_Science_Move cta=8gwifi.org/manic");
text(kicker, (cx, 112), "ROCKET PHYSICS · CUTAWAY");
text(headline, (cx, 174), "How does a rocket launch?");
text(caption, (cx, 1440), "A rocket carries two ingredients—not just fuel.");
size(kicker, 21); bold(kicker); color(kicker, cyan); hidden(kicker);
size(headline, 40); bold(headline); wrap(headline, 850); hidden(headline);
size(caption, 27); color(caption, dim); wrap(caption, 880); hidden(caption);
// One importer-safe cutaway asset. Its internal sections remain crisp at any size.
let rx = 540;
let ry = 760;
svg(rocket, (rx, ry), "asset:svg/physics/rocket-cutaway.svg", 300);
hidden(rocket); z(rocket, 5);
// Screen-authored highlights make each internal region independently teachable.
polygon(payloadHi, (480, 430), (540, 395), (600, 430), (600, 550), (480, 550));
rect(fuelHi, (540, 650), 120, 155);
rect(oxidizerHi, (540, 815), 120, 140);
polygon(chamberHi, (500, 900), (580, 900), (570, 955), (540, 980), (510, 955));
polygon(nozzleHi, (515, 975), (565, 975), (590, 1115), (490, 1115));
outlined(payloadHi); outlined(fuelHi); outlined(oxidizerHi); outlined(chamberHi); outlined(nozzleHi);
outline(payloadHi, cyan); outline(fuelHi, gold); outline(oxidizerHi, blue);
outline(chamberHi, magenta); outline(nozzleHi, fg);
stroke(payloadHi, 5); stroke(fuelHi, 5); stroke(oxidizerHi, 5); stroke(chamberHi, 5); stroke(nozzleHi, 5);
opacity(payloadHi, 0.9); opacity(fuelHi, 0.9); opacity(oxidizerHi, 0.9); opacity(chamberHi, 0.9); opacity(nozzleHi, 0.9);
hidden(payloadHi); hidden(fuelHi); hidden(oxidizerHi); hidden(chamberHi); hidden(nozzleHi);
// Clean callouts; the labels stay outside the vehicle silhouette.
line(payloadLead, (605, 470), (770, 420));
line(fuelLead, (600, 630), (790, 590));
line(oxidizerLead, (480, 805), (285, 780));
line(chamberLead, (500, 930), (280, 970));
line(nozzleLead, (575, 1045), (790, 1090));
color(payloadLead, cyan); color(fuelLead, gold); color(oxidizerLead, blue);
color(chamberLead, magenta); color(nozzleLead, fg);
stroke(payloadLead, 3); stroke(fuelLead, 3); stroke(oxidizerLead, 3); stroke(chamberLead, 3); stroke(nozzleLead, 3);
text(payloadLabel, (845, 395), "PAYLOAD");
text(fuelLabel, (855, 565), "FUEL");
text(oxidizerLabel, (195, 755), "OXIDIZER");
text(chamberLabel, (180, 950), "BURN");
text(nozzleLabel, (850, 1070), "NOZZLE");
size(payloadLabel, 22); size(fuelLabel, 22); size(oxidizerLabel, 22); size(chamberLabel, 22); size(nozzleLabel, 22);
bold(payloadLabel); bold(fuelLabel); bold(oxidizerLabel); bold(chamberLabel); bold(nozzleLabel);
color(payloadLabel, cyan); color(fuelLabel, gold); color(oxidizerLabel, blue);
color(chamberLabel, magenta); color(nozzleLabel, fg);
hidden(payloadLead); hidden(fuelLead); hidden(oxidizerLead); hidden(chamberLead); hidden(nozzleLead);
hidden(payloadLabel); hidden(fuelLabel); hidden(oxidizerLabel); hidden(chamberLabel); hidden(nozzleLabel);
// Reaction and force story.
equation(reaction, (cx, 1195), `\text{fuel}+\text{oxidizer}\longrightarrow\text{hot gas}`);
size(reaction, 34); hidden(reaction);
arrow(gasDown, (225, 1070), (225, 1300));
arrow(thrustUp, (850, 1120), (850, 880));
color(gasDown, magenta); color(thrustUp, cyan);
stroke(gasDown, 8); stroke(thrustUp, 8);
hidden(gasDown); hidden(thrustUp);
text(gasLabel, (170, 1320), "EXHAUST ↓");
text(thrustLabel, (930, 900), "THRUST ↑");
size(gasLabel, 22); size(thrustLabel, 22); bold(gasLabel); bold(thrustLabel);
color(gasLabel, magenta); color(thrustLabel, cyan);
hidden(gasLabel); hidden(thrustLabel);
polygon(flame, (500, 1090), (540, 1260), (580, 1090));
gradient(flame, gold, magenta, 90); filled(flame); glow(flame, 1.1); hidden(flame); z(flame, 4);
circle(exhaustSeed, (540, 1180), 12); opacity(exhaustSeed, 0); hidden(exhaustSeed);
particles(exhaust, exhaustSeed, 28, 6, 41, "random");
color(exhaust, gold); glow(exhaust, 0.8); hidden(exhaust);
line(exhaustPath, (540, 1170), (540, 1330)); opacity(exhaustPath, 0);
tag(rocket, vehicle);
tag(payloadHi, vehicle); tag(fuelHi, vehicle); tag(oxidizerHi, vehicle);
tag(chamberHi, vehicle); tag(nozzleHi, vehicle); tag(flame, vehicle);
// Creator chrome remains screen-safe while the vehicle moves.
sticky(kicker); sticky(headline); sticky(caption); sticky(mark);
sticky(me.footer); sticky(me.endcard);
step("hook") {
par {
show(kicker, 0.35);
show(headline, 0.50);
show(caption, 0.35);
show(rocket, 0.70);
speak("A rocket does not push against the ground. So what actually makes it rise?");
}
}
wait(0.35);
step("inside-the-rocket") {
par {
stagger(0.16) {
par { show(payloadHi, 0.25); show(payloadLead, 0.25); show(payloadLabel, 0.25); }
par { show(fuelHi, 0.25); show(fuelLead, 0.25); show(fuelLabel, 0.25); }
par { show(oxidizerHi, 0.25); show(oxidizerLead, 0.25); show(oxidizerLabel, 0.25); }
}
say(caption, "Fuel stores energy. Oxidizer supplies the oxygen needed to release it.", 0.40);
speak("Inside are the payload, fuel, and oxidizer. The oxidizer is why a rocket can burn in space.");
}
}
wait(0.30);
step("combustion") {
par {
show(chamberHi, 0.25); show(chamberLead, 0.25); show(chamberLabel, 0.25);
show(reaction, 0.50);
flash(chamberHi, magenta);
pulse(chamberHi, 0.80);
say(caption, "In the chamber, fuel and oxidizer become extremely hot, high-pressure gas.", 0.40);
speak("They burn together in the combustion chamber, creating hot, high pressure gas.");
}
}
cue(pop);
wait(0.25);
step("shape-the-exhaust") {
par {
fade(reaction, 0.25);
show(nozzleHi, 0.25); show(nozzleLead, 0.25); show(nozzleLabel, 0.25);
show(flame, 0.22);
show(exhaust, 0.15);
stream(exhaust, exhaustPath, 3.2, 34, smooth);
show(gasDown, 0.40); show(gasLabel, 0.30);
say(caption, "The nozzle accelerates that gas downward into a fast exhaust jet.", 0.40);
speak("The nozzle expands and accelerates the gas downward.");
}
}
cue(whoosh);
step("equal-and-opposite") {
par {
fade(payloadHi, 0.25); fade(fuelHi, 0.25); fade(oxidizerHi, 0.25);
fade(chamberHi, 0.25); fade(nozzleHi, 0.25);
fade(payloadLead, 0.25); fade(fuelLead, 0.25); fade(oxidizerLead, 0.25);
fade(chamberLead, 0.25); fade(nozzleLead, 0.25);
fade(payloadLabel, 0.25); fade(fuelLabel, 0.25); fade(oxidizerLabel, 0.25);
fade(chamberLabel, 0.25); fade(nozzleLabel, 0.25);
fade(reaction, 0.25); fade(exhaust, 0.25);
show(thrustUp, 0.40); show(thrustLabel, 0.30);
flash(thrustUp, cyan);
say(headline, "Gas goes down. The rocket goes up.", 0.45);
say(caption, "Newton's third law turns downward exhaust momentum into upward thrust.", 0.40);
speak("Gas goes down. An equal and opposite force pushes the rocket up.");
}
}
wait(0.30);
step("launch") {
par {
fade(kicker, 0.25); fade(headline, 0.25);
shift(vehicle, (0,-250), 4.0, smooth);
shift(thrustUp, (0,-180), 4.0, smooth);
shift(thrustLabel, (0,-180), 4.0, smooth);
stream(exhaust, exhaustPath, 4.0, 38, smooth);
fade(payloadLead, 0.25); fade(fuelLead, 0.25); fade(oxidizerLead, 0.25);
fade(chamberLead, 0.25); fade(nozzleLead, 0.25);
fade(payloadLabel, 0.25); fade(fuelLabel, 0.25); fade(oxidizerLabel, 0.25);
fade(chamberLabel, 0.25); fade(nozzleLabel, 0.25);
fade(reaction, 0.25); fade(gasDown, 0.25); fade(gasLabel, 0.25);
say(caption, "A rocket carries both sides of the reaction, so the process continues above the atmosphere.", 0.45);
speak("Because it carries its own oxidizer, the same thrust cycle keeps working in space.");
}
}
cue(chime);
wait(0.55);
step("creator-cta") {
par {
fade(kicker, 0.30); fade(headline, 0.30); fade(caption, 0.30);
fade(vehicle, 0.30); fade(exhaust, 0.30);
fade(thrustUp, 0.30); fade(thrustLabel, 0.30);
fade(me.footer, 0.30); fade(mark, 0.30);
}
show(me.endcard, 0.60);
}
wait(1.6);
creator-geometry-morph-promo
One persistent twelve-vertex actor becomes a triangle, square, diamond, hexagon, star,
hourglass, arrow, heart, bolt and crown. Equal topology makes the silhouettes interpolate
continuously while become, spin, say and pulse tell a fast geometry ident—then the
restrained Creator endcard closes the promo.
// Creator promo: one persistent twelve-vertex actor becomes ten readable
// silhouettes. Equal point counts make every transition a true geometric
// interpolation rather than a cut between unrelated drawings.
title("Geometry Never Sits Still");
canvas("9:16");
template("black");
creator(me, "@anish2good name=Manic_Geometry tagline=Geometry_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Make_geometry_move safe=clean");
socials(me);
watermark(mark, (w*0.18, h*0.08), "Made With Manic");
endcard(me, "title=Give_Your_Ideas_Motion cta=8gwifi.org/manic");
let u = (w+h-abs(w-h))/1080;
let ox = cx;
let oy = h*0.47;
let r = 205*u;
text(kicker, (cx,h*0.12), "MANIC · GEOMETRY IN MOTION");
text(head, (cx,h*0.21), "One shape. Endless ideas.");
text(shapeName, (cx,h*0.70), "DODECAGON");
text(caption, (cx,h*0.84), "The identity stays. Only the geometry changes.");
size(kicker,20*u); bold(kicker); color(kicker,cyan);
size(head,31*u); bold(head); wrap(head,w*0.78);
size(shapeName,23*u); bold(shapeName); color(shapeName,gold);
size(caption,20*u); bold(caption); color(caption,dim); wrap(caption,w*0.72);
// Twelve points in every blueprint: direct, continuous vertex motion.
polygon(hero,
(ox,oy-r), (ox+r*0.50,oy-r*0.866), (ox+r*0.866,oy-r*0.50),
(ox+r,oy), (ox+r*0.866,oy+r*0.50), (ox+r*0.50,oy+r*0.866),
(ox,oy+r), (ox-r*0.50,oy+r*0.866), (ox-r*0.866,oy+r*0.50),
(ox-r,oy), (ox-r*0.866,oy-r*0.50), (ox-r*0.50,oy-r*0.866));
gradient(hero, cyan, magenta, gold, 35); stroke(hero,4); glow(hero,0.22);
// Four copies of each corner preserve the twelve-point topology.
polygon(triangleForm,
(ox,oy-r), (ox,oy-r), (ox,oy-r), (ox,oy-r),
(ox+r*0.92,oy+r*0.72), (ox+r*0.92,oy+r*0.72), (ox+r*0.92,oy+r*0.72), (ox+r*0.92,oy+r*0.72),
(ox-r*0.92,oy+r*0.72), (ox-r*0.92,oy+r*0.72), (ox-r*0.92,oy+r*0.72), (ox-r*0.92,oy+r*0.72));
color(triangleForm,magenta); stroke(triangleForm,4); glow(triangleForm,0.18); hidden(triangleForm);
polygon(squareForm,
(ox-r*0.82,oy-r*0.82), (ox-r*0.82,oy-r*0.82), (ox-r*0.82,oy-r*0.82),
(ox+r*0.82,oy-r*0.82), (ox+r*0.82,oy-r*0.82), (ox+r*0.82,oy-r*0.82),
(ox+r*0.82,oy+r*0.82), (ox+r*0.82,oy+r*0.82), (ox+r*0.82,oy+r*0.82),
(ox-r*0.82,oy+r*0.82), (ox-r*0.82,oy+r*0.82), (ox-r*0.82,oy+r*0.82));
color(squareForm,blue); stroke(squareForm,4); glow(squareForm,0.18); hidden(squareForm);
polygon(diamondForm,
(ox,oy-r), (ox,oy-r), (ox,oy-r),
(ox+r,oy), (ox+r,oy), (ox+r,oy),
(ox,oy+r), (ox,oy+r), (ox,oy+r),
(ox-r,oy), (ox-r,oy), (ox-r,oy));
color(diamondForm,gold); stroke(diamondForm,4); glow(diamondForm,0.18); hidden(diamondForm);
polygon(hexagonForm,
(ox-r*0.52,oy-r*0.90), (ox-r*0.52,oy-r*0.90),
(ox+r*0.52,oy-r*0.90), (ox+r*0.52,oy-r*0.90),
(ox+r,oy), (ox+r,oy),
(ox+r*0.52,oy+r*0.90), (ox+r*0.52,oy+r*0.90),
(ox-r*0.52,oy+r*0.90), (ox-r*0.52,oy+r*0.90),
(ox-r,oy), (ox-r,oy));
color(hexagonForm,cyan); stroke(hexagonForm,4); glow(hexagonForm,0.18); hidden(hexagonForm);
polygon(starForm,
(ox,oy-r), (ox+r*0.23,oy-r*0.40), (ox+r*0.87,oy-r*0.50),
(ox+r*0.46,oy), (ox+r*0.87,oy+r*0.50), (ox+r*0.23,oy+r*0.40),
(ox,oy+r), (ox-r*0.23,oy+r*0.40), (ox-r*0.87,oy+r*0.50),
(ox-r*0.46,oy), (ox-r*0.87,oy-r*0.50), (ox-r*0.23,oy-r*0.40));
color(starForm,magenta); stroke(starForm,4); glow(starForm,0.22); hidden(starForm);
polygon(hourglassForm,
(ox-r*0.85,oy-r*0.90), (ox,oy-r*0.55), (ox+r*0.85,oy-r*0.90),
(ox+r*0.48,oy-r*0.20), (ox+r*0.16,oy), (ox+r*0.48,oy+r*0.20),
(ox+r*0.85,oy+r*0.90), (ox,oy+r*0.55), (ox-r*0.85,oy+r*0.90),
(ox-r*0.48,oy+r*0.20), (ox-r*0.16,oy), (ox-r*0.48,oy-r*0.20));
color(hourglassForm,violet); stroke(hourglassForm,4); glow(hourglassForm,0.18); hidden(hourglassForm);
polygon(arrowForm,
(ox-r,oy-r*0.28), (ox+r*0.18,oy-r*0.28), (ox+r*0.18,oy-r*0.62),
(ox+r,oy), (ox+r,oy), (ox+r,oy),
(ox+r*0.18,oy+r*0.62), (ox+r*0.18,oy+r*0.28), (ox-r,oy+r*0.28),
(ox-r,oy+r*0.10), (ox-r,oy), (ox-r,oy-r*0.10));
color(arrowForm,lime); stroke(arrowForm,4); glow(arrowForm,0.18); hidden(arrowForm);
polygon(heartForm,
(ox,oy+r), (ox-r*0.82,oy+r*0.15), (ox-r,oy-r*0.38),
(ox-r*0.70,oy-r*0.82), (ox-r*0.24,oy-r*0.78), (ox,oy-r*0.42),
(ox+r*0.24,oy-r*0.78), (ox+r*0.70,oy-r*0.82), (ox+r,oy-r*0.38),
(ox+r*0.82,oy+r*0.15), (ox+r*0.38,oy+r*0.62), (ox,oy+r));
color(heartForm,coral); stroke(heartForm,4); glow(heartForm,0.22); hidden(heartForm);
polygon(boltForm,
(ox+r*0.18,oy-r), (ox-r*0.62,oy+r*0.08), (ox-r*0.14,oy+r*0.02),
(ox-r*0.48,oy+r), (ox+r*0.62,oy-r*0.24), (ox+r*0.16,oy-r*0.18),
(ox+r*0.48,oy-r*0.76), (ox+r*0.34,oy-r*0.54), (ox+r*0.28,oy-r*0.36),
(ox+r*0.24,oy-r*0.20), (ox+r*0.21,oy-r*0.08), (ox+r*0.18,oy-r));
color(boltForm,gold); stroke(boltForm,4); glow(boltForm,0.22); hidden(boltForm);
polygon(crownForm,
(ox-r,oy+r*0.60), (ox-r*0.92,oy-r*0.45), (ox-r*0.46,oy+r*0.05),
(ox-r*0.30,oy-r), (ox,oy-r*0.18), (ox+r*0.30,oy-r),
(ox+r*0.46,oy+r*0.05), (ox+r*0.92,oy-r*0.45), (ox+r,oy+r*0.60),
(ox+r*0.42,oy+r*0.76), (ox-r*0.42,oy+r*0.76), (ox-r,oy+r*0.60));
color(crownForm,cyan); stroke(crownForm,4); glow(crownForm,0.22); hidden(crownForm);
hidden(kicker); hidden(head); hidden(shapeName); hidden(caption); hidden(hero);
step("one persistent shape") {
par {
show(kicker,0.30); show(head,0.45); show(shapeName,0.30);
show(caption,0.35); show(hero,0.55);
}
pulse(hero,0.55);
}
wait(0.30);
step("geometry keeps becoming") {
seq {
par { become(hero,triangleForm,0.62,smooth); say(shapeName,"TRIANGLE",0.22); }
par { become(hero,squareForm,0.62,smooth); say(shapeName,"SQUARE",0.22); spin(hero,45,0.62,smooth); }
par { become(hero,diamondForm,0.58,smooth); say(shapeName,"DIAMOND",0.22); }
par { become(hero,hexagonForm,0.62,smooth); say(shapeName,"HEXAGON",0.22); }
par { become(hero,starForm,0.70,smooth); say(shapeName,"STAR",0.22); spin(hero,30,0.70,smooth); }
par { become(hero,hourglassForm,0.66,smooth); say(shapeName,"HOURGLASS",0.22); }
par { become(hero,arrowForm,0.66,smooth); say(shapeName,"DIRECTION",0.22); }
par { become(hero,heartForm,0.72,smooth); say(shapeName,"HEART",0.22); }
par { become(hero,boltForm,0.66,smooth); say(shapeName,"ENERGY",0.22); }
par { become(hero,crownForm,0.72,smooth); say(shapeName,"STORY",0.22); }
}
}
wait(0.45);
step("the object never resets") {
par {
pulse(hero,0.80);
say(head,"Describe change—not keyframes.",0.40,smooth);
say(caption,"become · spin · recolor · pulse — one actor, one continuous idea.",0.45,smooth);
}
}
wait(1.00);
step("creator call to action") {
par {
fade(kicker,0.30); fade(head,0.30); fade(shapeName,0.30);
fade(caption,0.30); fade(hero,0.40); fade(me.footer,0.30);
show(me.endcard,0.60);
}
}
wait(2.00);
creator-geometry-language-showcase
How much olympiad geometry lives in one triangle? Four coloured midpoint regions prove an exact quarter-area identity; three altitude feet join the three side midpoints on the dashed nine-point circle; centroid, circumcenter, incenter, orthocenter and Euler line complete the story. Moving one free vertex proves the derived intersections and circles remain live.
// Creator showcase: one triangle grows from foundations into two olympiad
// results—four equal midpoint areas and the nine-point circle. Every derived
// construction remains live when vertex C moves.
title("How Much Geometry Lives in One Triangle?");
canvas("9:16");
template("black");
creator(me, "@anish2good name=Manic_Geometry tagline=Constructions_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Animate_the_proof safe=clean");
socials(me);
watermark(mark, (w*0.18, h*0.09), "Made With Manic");
endcard(me, "title=Build_The_Explanation cta=8gwifi.org/manic");
let u = (w+h-abs(w-h))/1080;
let g = (w+h-abs(w-h))*0.21;
let gy = h*0.56;
text(kicker,(cx,h*0.09),"MANIC · LIVE GEOMETRY");
text(head,(cx,h*0.18),"How much lives inside one triangle?");
text(counter,(cx,h*0.28),"01 · THREE FREE POINTS");
text(caption,(cx,h*0.755),"Begin with A, B and C.");
size(kicker,20*u); bold(kicker); color(kicker,cyan);
size(head,29*u); bold(head); wrap(head,w*0.80);
size(counter,20*u); bold(counter); color(counter,gold);
size(caption,21*u); bold(caption); color(caption,dim); wrap(caption,w*0.76);
// Free vertices: every construction below reads these three points.
point(A,(cx-g,gy+g*0.60),"A");
point(B,(cx+g,gy+g*0.60),"B");
point(C,(cx+g*0.12,gy-g),"C");
color(A,cyan); color(B,cyan); color(C,cyan);
hidden(A); hidden(B); hidden(C);
// 1–3: dynamic sides.
segment(ab,A,B); segment(bc,B,C); segment(ca,C,A);
color(ab,fg); color(bc,fg); color(ca,fg);
stroke(ab,3); stroke(bc,3); stroke(ca,3);
untraced(ab); untraced(bc); untraced(ca);
// 4: an angle mark.
anglemark(angleC,A,C,B); color(angleC,gold); stroke(angleC,4); untraced(angleC);
// 5–9: midpoint, median and centroid system.
midpoint(mAB,A,B); midpoint(mBC,B,C); midpoint(mCA,C,A);
color(mAB,lime); color(mBC,lime); color(mCA,lime);
label(mAB,"D",(-22*u,18*u)); label(mBC,"E",(20*u,14*u)); label(mCA,"F",(-22*u,14*u));
hidden(mAB); hidden(mBC); hidden(mCA);
// The midpoint triangle partitions ABC into four congruent, equal-area
// triangles. These fills are exact for the authored starting triangle and are
// intentionally dismissed before the later live vertex drag.
polygon(shadeA,
(cx-g,gy+g*0.60), (cx,gy+g*0.60), (cx-g*0.44,gy-g*0.20));
polygon(shadeB,
(cx+g,gy+g*0.60), (cx+g*0.56,gy-g*0.20), (cx,gy+g*0.60));
polygon(shadeC,
(cx+g*0.12,gy-g), (cx-g*0.44,gy-g*0.20), (cx+g*0.56,gy-g*0.20));
polygon(shadeMid,
(cx,gy+g*0.60), (cx+g*0.56,gy-g*0.20), (cx-g*0.44,gy-g*0.20));
filled(shadeA); color(shadeA,cyan); opacity(shadeA,0.24); hidden(shadeA);
filled(shadeB); color(shadeB,magenta); opacity(shadeB,0.24); hidden(shadeB);
filled(shadeC); color(shadeC,gold); opacity(shadeC,0.24); hidden(shadeC);
filled(shadeMid); color(shadeMid,violet); opacity(shadeMid,0.30); hidden(shadeMid);
segment(de,mAB,mBC); segment(ef,mBC,mCA); segment(fd,mCA,mAB);
color(de,gold); color(ef,gold); color(fd,gold);
stroke(de,3); stroke(ef,3); stroke(fd,3);
untraced(de); untraced(ef); untraced(fd);
equation(areaLaw,(cx,h*0.78),
`K_{ADF}=K_{BDE}=K_{CEF}=K_{DEF}`,
20*u);
equation(areaQuarter,(cx,h*0.86),`K=\frac14K_{ABC}`,22*u);
color(areaLaw,fg); color(areaQuarter,gold);
hidden(areaLaw); hidden(areaQuarter);
segment(medA,A,mBC); segment(medB,B,mCA); segment(medC,C,mAB);
color(medA,lime); color(medB,lime); color(medC,lime);
dashed(medA,10,8); dashed(medB,10,8); dashed(medC,10,8);
untraced(medA); untraced(medB); untraced(medC);
centroid(G,A,B,C); color(G,lime); label(G,"G",(22*u,-15*u)); hidden(G);
// 10–13: circumcenter/circumcircle and incenter/incircle.
circumcenter(O,A,B,C); color(O,magenta); label(O,"O",(-28*u,-15*u)); hidden(O);
circumcircle(omega,A,B,C); outline(omega,magenta); color(omega,panel);
stroke(omega,3); untraced(omega);
incenter(I,A,B,C); color(I,gold); label(I,"I",(22*u,15*u)); hidden(I);
incircle(gamma,A,B,C); outline(gamma,gold); color(gamma,panel);
stroke(gamma,3); untraced(gamma);
// 14–22: the three side intersections of the altitudes, their right-angle
// marks, the orthocenter, nine-point circle and Euler line.
foot(X,A,B,C); foot(Y,B,C,A); foot(Z,C,A,B);
color(X,blue); color(Y,blue); color(Z,blue);
label(X,"X",(18*u,15*u)); label(Y,"Y",(-20*u,15*u)); label(Z,"Z",(18*u,18*u));
hidden(X); hidden(Y); hidden(Z);
segment(altA,A,X); segment(altB,B,Y); segment(altC,C,Z);
color(altA,blue); color(altB,blue); color(altC,blue);
stroke(altA,3); stroke(altB,3); stroke(altC,3);
untraced(altA); untraced(altB); untraced(altC);
rightangle(squareX,A,X,B); rightangle(squareY,B,Y,C); rightangle(squareZ,C,Z,A);
color(squareX,blue); color(squareY,blue); color(squareZ,blue);
stroke(squareX,3); stroke(squareY,3); stroke(squareZ,3);
untraced(squareX); untraced(squareY); untraced(squareZ);
orthocenter(H,A,B,C); color(H,cyan); label(H,"H",(-28*u,15*u)); hidden(H);
circumcircle(nine,mAB,mBC,mCA); outline(nine,violet); color(nine,panel);
stroke(nine,4); dashed(nine,12,8); untraced(nine);
segment(euler,O,H); color(euler,cyan); stroke(euler,4); untraced(euler);
hidden(kicker); hidden(head); hidden(counter); hidden(caption);
step("three points become a question") {
par {
show(kicker,0.30); show(head,0.45); show(counter,0.30); show(caption,0.35);
}
stagger(0.16) { show(A,0.24); show(B,0.24); show(C,0.24); }
par { draw(ab,0.50,smooth); draw(bc,0.50,smooth); draw(ca,0.50,smooth); }
draw(angleC,0.42,smooth);
}
wait(0.35);
step("midpoints create four equal areas") {
seq {
par {
say(counter,"02 · OLYMPIAD AREA PARTITION",0.30);
say(caption,"Join the three side midpoints D, E and F. What happens to the four areas?",0.45,smooth);
}
stagger(0.13) { show(mAB,0.22); show(mBC,0.22); show(mCA,0.22); }
par { draw(de,0.55,smooth); draw(ef,0.55,smooth); draw(fd,0.55,smooth); }
stagger(0.10) {
show(shadeA,0.28); show(shadeB,0.28); show(shadeC,0.28); show(shadeMid,0.28);
}
par {
move(caption,(cx,h*0.70),0.35,smooth);
show(areaLaw,0.40); show(areaQuarter,0.40);
}
par {
pulse(shadeMid,0.60);
say(caption,"Each small triangle has one quarter of the original area.",0.40,smooth);
}
}
}
wait(0.60);
step("medians reveal the balance point") {
seq {
par {
fade(shadeA,0.28); fade(shadeB,0.28); fade(shadeC,0.28); fade(shadeMid,0.28);
fade(areaLaw,0.28); fade(areaQuarter,0.28);
fade(de,0.28); fade(ef,0.28); fade(fd,0.28);
move(caption,(cx,h*0.755),0.30,smooth);
say(counter,"03 · MEDIANS → CENTROID",0.30);
say(caption,"Now connect each vertex to the opposite midpoint. The three medians concur.",0.45,smooth);
}
par { draw(medA,0.62,smooth); draw(medB,0.62,smooth); draw(medC,0.62,smooth); }
show(G,0.28); pulse(G,0.60);
}
}
wait(0.45);
step("two circles find two centers") {
seq {
par {
fade(medA,0.30); fade(medB,0.30); fade(medC,0.30);
fade(mAB,0.30); fade(mBC,0.30); fade(mCA,0.30);
say(counter,"04 · TWO CENTERS, TWO CIRCLES",0.30);
say(caption,"O is equally far from the vertices. I is equally far from the sides.",0.45,smooth);
}
par { show(O,0.25); draw(omega,0.85,smooth); }
par { show(I,0.25); draw(gamma,0.75,smooth); }
par { pulse(O,0.55); pulse(I,0.55); }
}
}
wait(0.55);
step("six intersections reveal the nine point circle") {
seq {
par {
fade(omega,0.28); fade(O,0.28); fade(gamma,0.28); fade(I,0.28);
say(counter,"05 · SIX POINTS, ONE CIRCLE",0.30);
say(caption,"The three midpoints and three altitude feet all lie on one nine-point circle.",0.48,smooth);
}
stagger(0.12) { show(mAB,0.22); show(mBC,0.22); show(mCA,0.22); }
stagger(0.12) { show(X,0.22); show(Y,0.22); show(Z,0.22); }
par {
draw(altA,0.62,smooth); draw(altB,0.62,smooth); draw(altC,0.62,smooth);
draw(squareX,0.42,smooth); draw(squareY,0.42,smooth); draw(squareZ,0.42,smooth);
}
show(H,0.28); pulse(H,0.60);
draw(nine,1.05,smooth);
stagger(0.10) {
pulse(mAB,0.38); pulse(mBC,0.38); pulse(mCA,0.38);
pulse(X,0.38); pulse(Y,0.38); pulse(Z,0.38);
}
}
}
wait(0.70);
step("the Euler line connects the centers") {
seq {
par {
fade(nine,0.28);
fade(mAB,0.28); fade(mBC,0.28); fade(mCA,0.28);
fade(X,0.28); fade(Y,0.28); fade(Z,0.28);
fade(altA,0.28); fade(altB,0.28); fade(altC,0.28);
fade(squareX,0.28); fade(squareY,0.28); fade(squareZ,0.28);
say(counter,"06 · O, G AND H ALIGN",0.30);
say(caption,"Circumcenter, centroid and orthocenter lie on the Euler line.",0.42,smooth);
}
par { show(O,0.20); show(G,0.20); }
draw(euler,0.90,smooth);
stagger(0.16) { pulse(O,0.50); pulse(G,0.50); pulse(H,0.50); }
}
}
wait(0.55);
step("prove every construction is alive") {
seq {
par {
say(counter,"07 · MOVE ONE POINT",0.30);
say(caption,"Move C. Every midpoint, intersection, circle, center and line recomputes.",0.48,smooth);
recolor(C,gold,0.35);
}
par {
show(nine,0.25); show(mAB,0.20); show(mBC,0.20); show(mCA,0.20);
show(X,0.20); show(Y,0.20); show(Z,0.20);
}
move(C,(cx+g*0.60,gy-g*0.82),1.15,smooth);
move(C,(cx-g*0.48,gy-g*0.86),1.15,smooth);
move(C,(cx+g*0.12,gy-g),0.90,smooth);
pulse(C,0.55);
}
}
wait(0.80);
step("geometry becomes a language") {
par {
say(head,"Name the relationships. Manic makes them move.",0.45,smooth);
say(caption,"construct · intersect · shade · prove · deform — one continuous olympiad story",0.45,smooth);
pulse(euler,0.75);
}
}
wait(1.10);
step("creator call to action") {
par {
fade(kicker,0.30); fade(head,0.30); fade(counter,0.30); fade(caption,0.30);
fade(A,0.30); fade(B,0.30); fade(C,0.30);
fade(ab,0.30); fade(bc,0.30); fade(ca,0.30); fade(angleC,0.30);
fade(omega,0.35); fade(nine,0.35);
fade(mAB,0.30); fade(mBC,0.30); fade(mCA,0.30);
fade(X,0.30); fade(Y,0.30); fade(Z,0.30);
fade(O,0.30); fade(G,0.30); fade(H,0.30); fade(euler,0.35);
fade(me.footer,0.30); show(me.endcard,0.60);
}
}
wait(2.00);
creator-hilbert3-spatial-index
A cinematic use for hilbert3, unrelated to recursive cube filling: a scan plane resolves 64
translucent voxels, three red corner-to-corner jumps expose a poor address order, and one
fixed Hilbert route restores locality. Scanner, cache window, counter and orbiting camera
then travel together before H(x,y,z)=d lands the spatial-indexing payoff.
// A different use of hilbert3: spatial indexing, not curve refinement.
// Sixty-four 3D voxels receive one continuous address order. A moving cache
// window shows why axis-adjacent visits preserve useful spatial locality.
title("A 3D Volume, One Sortable Address");
canvas("9:16");
template("black");
creator(me,"@anish2good name=Manic_Data tagline=Spatial_ideas_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=gold footer=compact cta=Animate_the_algorithm safe=clean");
socials(me);
watermark(mark,(w*0.18,h*0.08),"Made With Manic");
endcard(me,"title=Turn_Data_Into_A_Story cta=8gwifi.org/manic");
let u=(w+h-abs(w-h))/1080;
text(kicker,(cx,h*0.14),"MANIC · SPATIAL INDEXING");
text(head,(cx,h*0.24),"How do you sort a 3D world?");
text(caption,(cx,h*0.82),"First, capture a world made from 64 separate voxels.");
counter(address,(cx,h*0.72),0,0,"VOXEL "," / 63");
equation(keyLaw,(cx,h*0.71),`H(x,y,z)=d`,30*u);
text(volumeReadout,(cx,h*0.72),"64 VOXELS · 3 COORDINATES");
text(jumpWarning,(cx,h*0.665),"NON-LOCAL ORDER · DISTANCE SPIKES");
text(localResult,(cx,h*0.72),"63 / 63 STEPS STAY FACE-ADJACENT");
text(probeLabel,(0,0),"ACTIVE CACHE");
size(kicker,20*u); bold(kicker); color(kicker,cyan);
size(head,31*u); bold(head); wrap(head,w*0.80);
size(caption,21*u); bold(caption); color(caption,dim); wrap(caption,w*0.76);
size(address,23*u); bold(address); color(address,gold);
size(volumeReadout,21*u); bold(volumeReadout); color(volumeReadout,dim);
size(jumpWarning,21*u); bold(jumpWarning); color(jumpWarning,red);
size(localResult,21*u); bold(localResult); color(localResult,lime);
size(probeLabel,17*u); bold(probeLabel); color(probeLabel,gold);
camera3((16,-18,14),(0,0,0),42);
// A 4×4×4 voxel volume. The centers match the order-2 Hilbert lattice:
// -2.1, -0.7, 0.7, 2.1 on each axis.
for z in 0..4 {
for y in 0..4 {
for x in 0..4 {
cube3(voxel{x}{y}{z},
((x-1.5)*1.4,(y-1.5)*1.4,(z-1.5)*1.4),
(1.12,1.12,1.12));
opacity(voxel{x}{y}{z},0.11);
hidden(voxel{x}{y}{z});
tag(voxel{x}{y}{z},voxels);
tag(voxel{x}{y}{z},layer{z});
}
}
}
color(layer0,blue);
color(layer1,cyan);
color(layer2,magenta);
color(layer3,gold);
// A cinematic acquisition plane sweeps upward while the volume resolves one
// layer at a time.
cube3(scanPlane,(0,0,-3.0),(5.6,5.6,0.07));
color(scanPlane,cyan); opacity(scanPlane,0.28); hidden(scanPlane);
// The route is used once as an address order—there is no recursive
// refinement and no "line fills a cube" reproduction.
hilbert3(route,(0,0,0),4.2,2,"color=gradient shade=depth");
thick(route,0.040);
hidden(route); untraced(route);
sphere3(scanner,(-2.1,-2.1,-2.1),0.19);
color(scanner,fg); hidden(scanner);
finish3(scanner,"shading=smooth material=metal mesh=0.18 shadow=0.75");
// A translucent neighborhood travels with the current address. It represents
// the small spatial working set a cache or chunked volume reader wants nearby.
cube3(cacheWindow,(-2.1,-2.1,-2.1),(1.34,1.34,1.34));
color(cacheWindow,gold); opacity(cacheWindow,0.16); hidden(cacheWindow);
finish3(cacheWindow,"shading=smooth material=glass mesh=0.10 depth=0.72 shadow=0.18");
// A deliberately poor address order: three long diagonals tear across the
// volume. It is visually dramatic because consecutive addresses are not
// spatial neighbours.
arrow3(jump0,(-2.1,-2.1,-2.1),(2.1,2.1,2.1));
arrow3(jump1,(2.1,2.1,2.1),(-2.1,2.1,-2.1));
arrow3(jump2,(-2.1,2.1,-2.1),(2.1,-2.1,2.1));
color(jump0,red); color(jump1,coral); color(jump2,red);
thick(jump0,0.045); thick(jump1,0.045); thick(jump2,0.045);
hidden(jump0); hidden(jump1); hidden(jump2);
untraced(jump0); untraced(jump1); untraced(jump2);
tag(jump0,badOrder); tag(jump1,badOrder); tag(jump2,badOrder);
pin3(probeLabel,scanner,(18,-18));
hidden(kicker); hidden(head); hidden(caption); hidden(address);
hidden(keyLaw); hidden(volumeReadout); hidden(jumpWarning); hidden(localResult);
hidden(probeLabel);
step("the world becomes data") {
par {
show(kicker,0.30); show(head,0.45); show(caption,0.40);
}
view3(voxels,"isometric",0.90,smooth,1.45);
cue(whoosh);
show(scanPlane,0.25);
show(volumeReadout,0.30);
par {
move3(scanPlane,(0,0,3.0),2.35,smooth);
stagger(0.46) {
show(layer0,0.34); show(layer1,0.34); show(layer2,0.34); show(layer3,0.34);
}
say(caption,"A scan resolves the physical world one layer at a time.",0.45,smooth);
}
fade(scanPlane,0.25);
}
wait(0.50);
step("the wrong order tears space apart") {
par {
fade(volumeReadout,0.25);
show(scanner,0.25); show(address,0.30); show(jumpWarning,0.30);
say(head,"A bad index destroys locality.",0.45,smooth);
say(caption,"Consecutive keys can jump from one corner of the volume to another.",0.45,smooth);
}
cue(whoosh);
par {
draw(jump0,0.80,smooth);
travel3(scanner,jump0,0.80,smooth);
to(address,value,63,0.80,smooth);
}
cue(whoosh);
par {
draw(jump1,0.70,smooth);
travel3(scanner,jump1,0.70,smooth);
to(address,value,3,0.70,smooth);
}
cue(whoosh);
par {
draw(jump2,0.75,smooth);
travel3(scanner,jump2,0.75,smooth);
to(address,value,60,0.75,smooth);
}
shake(jumpWarning,0.45);
}
wait(0.55);
step("hilbert order restores locality") {
par {
fade(badOrder,0.35); fade(jumpWarning,0.25);
move3(scanner,(-2.1,-2.1,-2.1),0.65,smooth);
to(address,value,0,0.65,smooth);
say(head,"One route restores locality.",0.45,smooth);
say(caption,"Hilbert order makes every next address share a face with the last.",0.45,smooth);
}
cue(chime);
par {
show(route,0.20); draw(route,1.80,smooth);
show(cacheWindow,0.30); show(probeLabel,0.30);
}
pulse(scanner,0.55);
}
wait(0.45);
step("one uninterrupted spatial journey") {
attach3(cacheWindow,scanner,(0,0,0));
par {
travel3(scanner,route,7.20,linear);
to(address,value,63,7.20,linear);
orbit3(132,30,29.0,7.20,smooth);
say(caption,"The cache window, address and camera now travel as one synchronized shot.",0.48,smooth);
}
attach3(cacheWindow,none);
}
wait(0.55);
step("the payoff") {
par {
fade(address,0.25); fade(probeLabel,0.25); fade(cacheWindow,0.30);
show(localResult,0.35);
pulse(route,0.80);
say(head,"Locality wins.",0.45,smooth);
say(caption,"All 64 voxels are visited—and all 63 transitions stay face-adjacent.",0.50,smooth);
}
}
wait(1.00);
step("turn location into a key") {
par {
fade(localResult,0.25);
show(keyLaw,0.40);
pulse(route,0.80);
say(head,"Three coordinates become one sortable key.",0.45,smooth);
say(caption,"One idea powers medical volumes, spatial databases, simulations and maps.",0.50,smooth);
}
}
wait(1.35);
step("creator call to action") {
par {
fade(kicker,0.30); fade(head,0.30); fade(caption,0.30); fade(keyLaw,0.30);
fade(scanner,0.30); fade(route,0.40); fade(voxels,0.40); fade(badOrder,0.30);
fade(volumeReadout,0.25); fade(jumpWarning,0.25); fade(localResult,0.25);
fade(me.footer,0.30); show(me.endcard,0.60);
}
}
wait(2.00);
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");
watermark(manicMark, (w*0.895-100, h*0.075+24), "Made With Manic");
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");
watermark(manicMark, (w*0.91-100, h*0.055+24), "Made With Manic");
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");
watermark(manicMark, (w*0.91-100, h*0.055+24), "Made With Manic");
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");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
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");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
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");
watermark(manicMark, (w*0.91-100, h*0.055+24), "Made With Manic");
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");
watermark(manicMark, (w*0.91-100, h*0.055+24), "Made With Manic");
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");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
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");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
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);
creator-race-quiz
The charts kit composed with everything else: a voiced 9:16 Short that poses a QUIZ (which
economy grew fastest since 1990?), reveals the answer, then plays a bar-chart RACE that
PROVES it — China climbing from the pack to #2. Quiz cards + speak narration over a
par-run race + a Manic CTA card. Problem → guess → proof → CTA.
// creator-race-quiz.manic — a Creator Short that PROVES a claim with a race chart.
// Poses a quiz (which economy grew fastest since 1990?), lets you guess, reveals
// the answer, then plays a bar-chart RACE that settles it — the data as the payoff.
// Shows the charts kit composing with quiz cards, voice, and creator chrome.
//
// ELEVENLABS_API_KEY=... manic examples/creator-race-quiz.manic --record out.mp4
title("The Race-Chart Quiz");
canvas("9:16");
template("shorts");
voice("elevenlabs");
creator(me, "@anish2good name=Manic tagline=Turn_plain_text_into_amazing_data_videos yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Try_it_free safe=reels");
watermark(mark, (w*0.955-100, h*0.045+24), "Made With Manic");
// --- the question ---
text(kick, (cx, h*0.13), "Since 1990, which economy grew the FASTEST?");
size(kick, 46); color(kick, fg); bold(kick); wrap(kick, w*0.86); hidden(kick);
// --- four option cards (India · China · S. Korea · Brazil) ---
let cy0 = h*0.30; let ch = h*0.09; let gap = h*0.115;
rect(card1, (cx, cy0), w*0.74, ch); outlined(card1); outline(card1, dim); stroke(card1, 3); hidden(card1);
rect(card2, (cx, cy0+gap), w*0.74, ch); outlined(card2); outline(card2, dim); stroke(card2, 3); hidden(card2);
rect(card3, (cx, cy0+gap*2), w*0.74, ch); outlined(card3); outline(card3, dim); stroke(card3, 3); hidden(card3);
rect(card4, (cx, cy0+gap*3), w*0.74, ch); outlined(card4); outline(card4, dim); stroke(card4, 3); hidden(card4);
text(lbl1, (cx, cy0), "India"); size(lbl1, 40); color(lbl1, fg); bold(lbl1); hidden(lbl1);
text(lbl2, (cx, cy0+gap), "China"); size(lbl2, 40); color(lbl2, fg); bold(lbl2); hidden(lbl2);
text(lbl3, (cx, cy0+gap*2), "South Korea"); size(lbl3, 40); color(lbl3, fg); bold(lbl3); hidden(lbl3);
text(lbl4, (cx, cy0+gap*3), "Brazil"); size(lbl4, 40); color(lbl4, fg); bold(lbl4); hidden(lbl4);
// --- the proof: a GDP bar-chart race (labels only), hidden until the reveal ---
racechart(gdp, "bar", "1990 1998 2006 2014 2020 2026");
racedata(gdp, "
USA, 5.96, 8.6, 13.8, 17.5, 21.1, 30.3
China, 0.36, 1.1, 2.8, 10.5, 14.7, 20.5
Japan, 3.13, 4.1, 4.6, 4.9, 5.1, 4.4
India, 0.32, 0.4, 0.9, 2.0, 2.7, 4.6
Brazil, 0.46, 0.9, 1.1, 2.5, 1.5, 2.4
South Korea, 0.28, 0.6, 1.0, 1.5, 1.6, 1.9
");
text(cap, (cx, h*0.70), ""); size(cap, 34); color(cap, dim); bold(cap); wrap(cap, w*0.86); hidden(cap);
socials(me);
endcard(me, "title=Race_ANY_Dataset cta=8gwifi.org/manic");
// ---------------- the story ----------------
fade(gdp, 0.01); // keep the race hidden during the quiz
show(kick);
speak("Since nineteen-ninety, one economy grew faster than every other major country. Which one?");
stagger(0.16) { show(card1); show(card2); show(card3); show(card4); }
par { show(lbl1); show(lbl2); show(lbl3); show(lbl4); }
show(cap, 0.3); say(cap, "Lock in your guess…");
wait(1.0);
speak("The answer? China.");
par { recolor(card2, gold); pulse(card2); recolor(lbl2, gold); }
par { fade(card1); fade(card3); fade(card4); fade(lbl1); fade(lbl3); fade(lbl4); }
wait(0.6);
speak("But don't take my word for it — let the data settle it.");
par { fade(card2); fade(lbl2); fade(kick); fade(cap); }
show(gdp);
par {
race(gdp, 8.0);
seq {
speak("Watch China — from the bottom of the pack,");
speak("climbing past Brazil, Korea, India, even Japan,");
speak("all the way to number two.");
}
}
wait(0.6);
speak("The data doesn't argue. And this whole video is one Manic script — drop in any dataset, get a race. Free, at eight g wifi dot org slash manic.");
par { fade(gdp); }
show(me.endcard);
wait(2.6);
Race charts — paste a table, get a race
The charts kit turns a table into an animated race chart — ranked bars, columns or lines that reorder over time as the data changes. Declare a racechart, paste the data with racedata (a value per period per row; icons ride the bars), and play with race. Bars get rescaling gridlines + a live Total; raceline/racepanel add a companion line or a multi-line history panel below the bars. See the charts guide.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
charts-gdp-race
The flagship bar-chart race: the world’s biggest economies, GDP 1990→2026, with country flags riding the bars. Watch China climb from #7 to #2 as the bars reorder, the axis rescales with gridlines, and the year + Total tick — all from one pasted data block.
// charts-gdp-race.manic — an animated BAR-CHART RACE from a pasted data block.
//
// The `charts` kit turns a table into bars that actually REORDER as the numbers
// change — the viral data-viz format, but true to the data and self-contained in
// one script. Paste your data into `racedata` (one row per entity: label, an icon
// shorthand, then a value per period), then `race` plays the whole thing —
// re-ranking, sliding bars to their new slots, rescaling the axis, and ticking the
// year + values. Bars are rounded, gradient-filled; flags are imported SVG.
//
// Nominal GDP, US$ trillion, 1990 → 2026 (approx.) — watch China climb from the
// bottom to #2, Japan slide, and India rise.
//
// manic examples/charts-gdp-race.manic
canvas("16:9");
template("blank");
racechart(gdp, "bar", "1990 1995 2000 2005 2010 2015 2020 2026",
"The World's Biggest Economies — GDP 1990 to 2026 ($T)");
// paste your table: label, ICON, then one value per period.
// separators are forgiving — commas here, but tabs / spaces / newlines all work.
//
// the ICON column takes ANY svg, not just flags:
// us a flag shorthand → asset:svg/flags/us.svg
// rocket / heart / trophy an emoji/icon alias → asset:svg/emoji|lucide/…
// asset:svg/lucide/lightbulb.svg an explicit set path
// logos/acme.svg your OWN file (any path ending in .svg)
// omit the icon entirely and the row is just label + values.
//
// NOTE: a few flags whose stars are drawn with SVG <use> (e.g. cn — China) render
// imperfectly for now (tracked); most flags and all icons/emoji render cleanly.
racedata(gdp, "
USA, us, 5.96, 7.64, 10.25, 13.04, 15.05, 18.24, 21.06, 30.34
China, cn, 0.36, 0.73, 1.21, 2.29, 6.09, 11.06, 14.69, 20.50
Japan, jp, 3.13, 5.55, 4.97, 4.83, 5.76, 4.44, 5.06, 4.40
Germany, de, 1.60, 2.59, 1.95, 2.85, 3.40, 3.36, 3.89, 4.90
India, in, 0.32, 0.36, 0.47, 0.82, 1.68, 2.10, 2.67, 4.60
UK, gb, 1.09, 1.33, 1.66, 2.54, 2.49, 2.93, 2.70, 3.90
France, fr, 1.27, 1.60, 1.36, 2.20, 2.65, 2.44, 2.63, 3.30
Brazil, br, 0.46, 0.77, 0.65, 0.89, 2.21, 1.80, 1.48, 2.40
");
race(gdp, 14);
charts-column-race
A COLUMN race (vertical bars), built with the per-series raceseries input: men’s tennis
Grand Slam titles 2005→2024, Djokovic climbing to overtake Federer and Nadal, with flags.
// charts-column-race.manic — a COLUMN chart race (vertical bars), built with the
// per-series `raceseries` input (the computed/loop sibling of `racedata`). A sport
// race: the men's tennis Grand Slam count, 2005 → 2024 — watch Djokovic climb from
// nothing to overtake Federer and Nadal. Flags are imported SVG (rs/es/ch/gb).
//
// manic examples/charts-column-race.manic
canvas("16:9");
template("blank");
racechart(gs, "column", "2005 2009 2013 2017 2021 2024",
"Men's Tennis — Grand Slam Titles, 2005 to 2024");
// one entity per player: raceseries(chart, "name", "flag", "v0 v1 …")
raceseries(gs, "Federer", "ch", "5 15 17 19 20 20");
raceseries(gs, "Nadal", "es", "4 6 13 16 20 22");
raceseries(gs, "Djokovic", "rs", "0 1 6 12 20 24");
raceseries(gs, "Murray", "gb", "0 0 2 3 3 3");
race(gs, 10);
charts-line-race
A LINE race: each series draws its line on left-to-right as the years tick, value/label/flag riding the leading tip; the race is which line climbs highest. Fixed axis, decluttered tips.
// charts-line-race.manic — a LINE chart race. Each series draws its line on
// (left → right) as the years tick, with the value, label and flag riding the
// leading tip; the "race" is which line climbs highest. Fixed y-axis (line charts
// don't rescale), L-shaped axes, flags are imported SVG.
//
// manic examples/charts-line-race.manic
canvas("16:9");
template("blank");
racechart(gdp, "line", "1990 2000 2010 2020 2026",
"GDP Over Time — a Line Race ($T)");
racedata(gdp, "
USA, us, 5.96, 10.25, 15.05, 21.06, 30.34
China, cn, 0.36, 1.21, 6.09, 14.69, 20.50
Japan, jp, 3.13, 4.97, 5.76, 5.06, 4.40
Germany, de, 1.60, 1.95, 3.40, 3.89, 4.90
India, in, 0.32, 0.47, 1.68, 2.67, 4.60
");
race(gdp, 10);
charts-bar-line
The bar+line combo: a GDP bar race with a companion raceline across the top showing the
auto-summed world Total, drawn on with a marker and live readout, synced to the race.
// charts-bar-line.manic — a BAR race with a companion LINE (the classic combo).
//
// The bars race as usual (reordering by value), and `raceline` draws a synced line
// across the top strip — here the running TOTAL, auto-summed from the bars (pass
// your own "v0 v1 …" to track a different metric). The line draws on, a marker
// rides its leading point, and a live readout counts up — all locked to the race.
//
// manic examples/charts-bar-line.manic
canvas("16:9");
template("blank");
racechart(gdp, "bar", "1990 2000 2010 2020 2026",
"World GDP — Who's Biggest, and the Total Growing ($T)");
racedata(gdp, "
USA, us, 5.96, 10.25, 15.05, 21.06, 30.34
China, cn, 0.36, 1.21, 6.09, 14.69, 20.50
Japan, jp, 3.13, 4.97, 5.76, 5.06, 4.40
Germany, de, 1.60, 1.95, 3.40, 3.89, 4.90
India, in, 0.32, 0.47, 1.68, 2.67, 4.60
");
// the companion line across the top — a running total of all bars (auto-summed)
raceline(gdp, "World total");
race(gdp, 12);
charts-german-elections
Bar race + a multi-line history racepanel below (the Flourish bar+line layout), from a
REAL CSV: German federal election vote share 1949→2021, 7 parties, missing early years as
0, Total 100. Every party is a bar up top and a color-matched line in the history panel.
// charts-german-elections.manic — a BAR race from real CSV data (German federal
// election vote share, %, 1949-2021). Transcribed from Data.1785183258253.csv:
// the empty separator column is dropped and missing early years (parties that
// didn't exist yet) are 0. Watch CDU/CSU and SPD trade the lead while the Greens,
// Left and AfD climb from nothing.
//
// manic examples/charts-german-elections.manic
canvas("16:9");
template("blank");
racechart(de, "bar", "1949 1953 1957 1961 1965 1969 1972 1976 1980 1983 1987 1990 1994 1998 2002 2005 2009 2013 2017 2021",
"German Federal Elections — Vote Share %, 1949 to 2021");
racedata(de, "
CDU/CSU, 31, 45.2, 50.2, 45.3, 47.6, 46.1, 44.9, 48.6, 44.5, 48.8, 44.3, 43.8, 41.4, 35.1, 38.5, 35.2, 33.8, 41.5, 32.9, 24.1
SPD, 29.2, 28.8, 31.8, 36.2, 39.3, 42.7, 45.8, 42.6, 42.9, 38.2, 37, 33.5, 36.4, 40.9, 38.5, 34.2, 23, 25.7, 20.5, 25.7
FDP, 11.9, 9.5, 7.7, 12.8, 9.5, 5.8, 8.4, 7.9, 10.6, 7, 9.1, 11, 6.9, 6.2, 7.4, 9.8, 14.6, 4.8, 10.7, 11.5
Green, 0, 0, 0, 0, 0, 0, 0, 0, 1.5, 5.6, 8.3, 5.1, 7.3, 6.7, 8.6, 8.1, 10.7, 8.4, 8.9, 14.8
Left, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2.4, 4.4, 5.1, 4, 8.7, 11.9, 8.6, 9.2, 4.9
AfD, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4.7, 12.6, 10.3
Other, 27.6, 16.3, 10.2, 5.6, 3.6, 5.4, 0.9, 0.9, 0.5, 0.4, 1.3, 4.2, 3.6, 6, 3, 4, 6, 6.3, 5.2, 8.6
");
// a synced multi-line history panel below the bars: every party as a line, drawn
// up to a moving time-cursor (the Flourish bar+line combo)
racepanel(de);
race(de, 16);
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-v2-lab uses the bundled geometry-only asset asset:models/manic-pyramid.obj; production packages install it automatically, or you can replace its model3 call with a built-in solid.
engine-test-14-laplace
A 3Blue1Brown-style Laplace Transform board, showcasing the domain-colouring toolkit:
the |F(s)| pole surface is a domainsurface (height = magnitude, colour = phase) rendered
INSIDE the middle panel via the new camera3 viewport, flanked by the Re[e^{st}] waveform,
the s-plane vector, and the e^{st} spiral — three graphs kept in sync by one complex-frequency
parameter (bind on a reactive param curve). Colour-coded formulas, a directional brace,
a colorwheel legend, an svg() pi-creature, and a domaincolor-family palette throughout.
// ============================================================================
// engine-test-14-laplace.manic — reproducing the 3Blue1Brown "Laplace
// Transform" clip (engine-test-14-1.mp4) as a two-scene story:
// SCENE 1 the Laplace board — a brace over the left rectangle, whose THREE
// graphs (Re[e^{st}] waveform · s-plane vector to s · e^{st} spiral)
// stay synced to one complex-frequency parameter s; the |F(s)| pole
// surface (domainsurface) in the middle camera3 VIEWPORT with its
// cos(t)e^{-st} insets; the s-plane contour + colorwheel on the right.
// TRANSITION the board fades and Scene 2 EMERGES in the left rectangle with
// MOTION (continuity, not a hard cut — motion-graphics.md): e^{pi i}=-1
// surfaces and the red pi RISES up into the unit circle as the cue.
// SCENE 2 Euler's identity — e^t + axes, the unit circle with pi, e^{pi i}=-1,
// and i^2 = -1.
// (Graphs sized to fit their panels — clip doesn't compose with a camera3
// viewport panel; see CAPABILITIES.)
// ============================================================================
title("Laplace Transform — engine test 14");
canvas(1280, 720);
template("black");
// ---- masthead ----
svg(creature, (108, 108), "asset:svg/pi-creature.svg", 120);
equation(ttl, (820, 74), `\text{Laplace Transform}`, 44); color(ttl, fg);
equation(fwd, (430, 178),
`F(\textcolor{gold}{s})=\int_{0}^{\infty} f(\textcolor{cyan}{t})\,e^{-\textcolor{gold}{s}\textcolor{cyan}{t}}\,dt`, 26);
equation(inv, (930, 178),
`f(\textcolor{cyan}{t})=\tfrac{1}{2\pi i}\int_{a-i\infty}^{a+i\infty} F(\textcolor{gold}{s})\,e^{\textcolor{gold}{s}\textcolor{cyan}{t}}\,d\textcolor{gold}{s}`, 21);
// ---- three bordered panels ----
rect(pL, (222, 500), 372, 320); outlined(pL); outline(pL, dim);
rect(pM, (640, 500), 372, 320); outlined(pM); outline(pM, dim);
rect(pR, (1058, 500), 372, 320); outlined(pR); outline(pR, dim);
// a brace grouping the LEFT rectangle (the signal analysis of f(t))
brace(br, (44, 324), (400, 324), "down"); color(br, dim);
// ---- LEFT panel: three graphs, all driven by the complex frequency s = -0.1 + i·(p) ----
parameter(sfreq, (222, 648), -1.8, -1.8, -0.3, "Im s", 1);
// 1) the damped Re[e^{st}] waveform
plot(wave, (50, 412), 11, 26, "exp(-0.1*x)*cos(p*x)", (0, 30));
color(wave, mint); stroke(wave, 2.4);
bind(sfreq, wave, formula, "exp(-0.1*x)*cos(p*x)");
equation(rel, (95, 372), `\mathrm{Re}\,[e^{st}]`, 15); color(rel, mint);
// 2) the s-plane, with a vector to the point s = -0.1 - 0.8i (its tip tracks s)
complexplane(spA, (132, 578), 62, 62, 30);
arrow(svec, (132, 578), (129, 632)); color(svec, gold);
// 3) the e^{st} spiral (a reactive param curve)
param(spir, (318, 578), 22, 22, "exp(-0.1*t)*cos(p*t)", "exp(-0.1*t)*sin(p*t)", (0, 26));
color(spir, cyan); stroke(spir, 2.0);
bind(sfreq, spir, formula, "exp(-0.1*t)*cos(p*t)", "exp(-0.1*t)*sin(p*t)");
equation(est, (360, 520), `e^{st}`, 16); color(est, gold);
// ---- MIDDLE panel: the |F(s)| modulus surface, domain-coloured, in a viewport ----
camera3((7.2, -6.4, 5.0), (0.15, 0.0, 0.9), 40, perspective, (640, 500), 372, 320);
domainsurface(surf, "1/(z*z+1)", (-2.5, 2.5), (-2.5, 2.5), 64, 0.26);
equation(fs, (592, 640), `|F(\textcolor{gold}{s})|`, 16); color(fs, dim);
// insets over the panel: the integrand cos(t)·e^{-st} (a rose) and its transform
equation(rlab, (540, 392), `\cos(t)\,e^{-st}`, 14); color(rlab, magenta);
param(rose, (540, 430), 20, 20, "cos(t)*exp(-0.1*t)*cos(p*t)", "-cos(t)*exp(-0.1*t)*sin(p*t)", (0, 18));
color(rose, magenta); stroke(rose, 1.6);
bind(sfreq, rose, formula, "cos(t)*exp(-0.1*t)*cos(p*t)", "-cos(t)*exp(-0.1*t)*sin(p*t)");
equation(ilab, (738, 392), `\int_{0}^{\infty}\!\cos(t)\,e^{-st}dt`, 12); color(ilab, gold);
param(iarc, (738, 430), 26, 26, "t/(t*t+1)", "0-1/(t*t+1)", (0.2, 3.2));
color(iarc, gold); stroke(iarc, 1.6);
// ---- RIGHT panel: the s-plane contour + the colour-wheel legend ----
complexplane(cpl, (1058, 500), 120, 120, 40);
plot(contour, (1058, 500), 40, 22, "2*sin(x*2.2)/(1+x*x)", (-2.5, 2.5));
color(contour, coral); stroke(contour, 2.2);
colorwheel(key, (1205, 648), 30);
equation(keyl, (1152, 610), `\arg`, 15); color(keyl, dim);
// ---- Scene 2 (Euler's identity) — hidden; revealed as the board fades ----
// a bordered LEFT rectangle framing the identity, mirroring the grid's frame so
// the two panels read as a balanced pair (reference img_22/img_23).
rect(eboard, (224, 400), 420, 400); outlined(eboard); outline(eboard, dim); tag(eboard, euler);
axes(eax, (95, 487), 66, 62); tag(eax, euler);
plot(ecurve, (95, 487), 18, 16, "exp(x)", (-4, 2.2)); color(ecurve, cyan); stroke(ecurve, 2.2); tag(ecurve, euler);
// the derivative demo: a white tangent that SLIDES along e^t (its slope = its height),
// with the d/dt e^t=e^t caption riding along — matching the reference's white styling.
tangent(etan, ecurve, 0.4, 96); color(etan, fg); stroke(etan, 2.5); tag(etan, euler);
equation(edl, (150, 455), `\tfrac{d}{dt}e^{t}=e^{t}`, 14); color(edl, fg); tag(edl, euler);
// the slope triangle under the tangent: run "1" + red rise = e^t, so slope = height.
// Parts share tag `etri`, so to(etri, x, ...) slides them WITH the tangent.
slopetri(etri, ecurve, 0.4, 1); hidden(etri);
equation(erise, (176, 420), `e^{t}`, 13); color(erise, red); tag(erise, euler);
param(ecirc, (250, 400), 34, 34, "cos(t)", "sin(t)", (0, 6.2832)); color(ecirc, dim); stroke(ecirc, 2.0); tag(ecirc, euler);
param(earc, (250, 400), 34, 34, "cos(t)", "sin(t)", (0, 3.14159)); color(earc, red); stroke(earc, 2.4); tag(earc, euler);
equation(epi, (250, 424), `\pi`, 16); color(epi, red); tag(epi, euler); // starts low; rises in the transition
line(erad, (250, 400), (284, 400)); color(erad, dim); tag(erad, euler);
// 3b1b colour convention: i is always cyan, π is always red (same reds/cyans as the
// circle's π-arc and this i), so the eye tracks each symbol across the identities.
equation(eisq, (352, 396), `\textcolor{cyan}{i}^{2}=-1`, 20); tag(eisq, euler);
equation(eeuler, (214, 540), `e^{\textcolor{red}{\pi}\textcolor{cyan}{i}}=-1`, 24); tag(eeuler, euler);
hidden(euler);
// ---- Scene 3 (00:05–00:17): the SHM phase grid — a 5×5 sweep of the mass-spring
// oscillator over initial position × initial velocity. `sweep` rebuilds the
// oscillator you authored below once per cell, varying two of its OWN named
// parameters (`x0`, `v0`); `run` animates all 25 in sync. Hidden until Euler
// settles, then swept in with the teal arrow. ----
spring(phzcell, (0, 0), 10, 0, 120, 0, 0); hidden(phzcell);
sweep(phz, phzcell, x0, (-2, 2), v0, (2, -2), (895, 400), 5, 5, 150, 80, 0, 0);
hidden(phz);
// a bold teal CURVED arrow arcing from the board up-and-over into the grid; drawn
// on (untraced → draw) so the arc sweeps in and the eye rides it into Scene 3.
arrow(swp, (352, 228), (600, 206), -88); color(swp, mint); stroke(swp, 6); untraced(swp);
// ---- Scene 4 (00:17–00:23): the exponential e^{st} with the REAL rate s swept —
// decay (s<0) → flat (s=0) → growth (s>0). One `plot` bound to a `parameter`;
// the title's live gold number is a `counter` bound to the same s. Hidden. ----
axes(gax, (780, 400), 270, 200); color(gax, dim); tag(gax, graph);
plot(gcurve, (780, 400), 60, 38, "exp(p*x)", (-3.6, 3.8)); color(gcurve, mint); stroke(gcurve, 3); tag(gcurve, graph);
equation(gyl, (816, 206), `e^{st}`, 15); color(gyl, dim); tag(gyl, graph);
equation(gxl, (1058, 412), `t`, 18); color(gxl, dim); tag(gxl, graph);
parameter(gs, (600, 690), -0.16, -0.95, 0.5, "", 2); // slider hidden — it only drives
bind(gs, gcurve, formula, "exp(p*x)");
// live colour-coded title e^{ [s] t } — e white · s gold (a counter) · t cyan
equation(gte, (248, 112), `e`, 56); color(gte, fg); tag(gte, graph);
counter(gsc, (300, 74), -0.16, 2, "", ""); color(gsc, gold); size(gsc, 36); tag(gsc, graph);
equation(gtt, (400, 76), `t`, 34); color(gtt, cyan); tag(gtt, graph);
bind(gs, gsc, value, "p");
// clean single-equation symbolic title — crisp LaTeX superscript (used once s is symbolic)
equation(esym, (250, 98), `e^{\textcolor{gold}{s}\textcolor{cyan}{t}}`, 40); hidden(esym);
hidden(graph); hidden(gs.widget);
// ---- Scene 5 (00:24–00:37): s lives on a NUMBER LINE — slide it, the graph reshapes.
// The graph sits on the right; a labelled real axis for s appears on the left with a
// gold marker + "s = value" readout (a counter bound to the same s). ----
numberline(sl, (240, 400), 155, -2, 2, 1); // labelled real axis −2..2
rect(slbox, (240, 380), 380, 140); outlined(slbox); outline(slbox, gold);
counter(sread, (240, 332), 0.5, 1, "s = ", ""); color(sread, gold); size(sread, 26);
bind(gs, sread, value, "p");
dot(smk, (279, 400), 8); color(smk, gold); // marker rides s: x = 240 + 77.5·s
hidden(sl); hidden(slbox); hidden(sread); hidden(smk);
// ---- Scene 6 (00:37–00:53): s is a POINT IN THE COMPLEX PLANE, and e^{st} SPIRALS.
// CONTINUITY: the LEFT plane sits exactly on the number line (same centre + unit 77.5),
// so the real axis persists and the s-marker LIFTS off ℝ into ℂ. RIGHT traces e^{st} as
// t grows — a spiral (rotation from Im s, decay from Re s). The payoff of the story. ----
complexplane(lp, (240, 400), 176, 158, 77.5); // real axis = the number line
equation(lph, (240, 214), `\text{Complex Plane}`, 22); color(lph, fg);
equation(sval, (352, 316), `s = -0.2 + 1.0i`, 17); color(sval, gold); // smk lifts here → its label
complexplane(rp, (930, 400), 176, 158, 77.5);
param(spir6, (930, 400), 77.5, 77.5, "exp(-0.2*t)*cos(t)", "exp(-0.2*t)*sin(t)", (0, 15)); color(spir6, lime); stroke(spir6, 2.5); untraced(spir6);
equation(whyt, (640, 72), `\text{Why}`, 48); color(whyt, fg); hidden(whyt);
equation(ctitle, (640, 72), `\text{Wait, what does this even mean?}`, 30); color(ctitle, fg);
svg(pc1, (300, 650), "asset:svg/pi-creature.svg", 84);
svg(pc2, (470, 650), "asset:svg/pi-creature.svg", 84);
svg(pc3, (820, 650), "asset:svg/pi-creature.svg", 84);
svg(pc4, (990, 650), "asset:svg/pi-creature.svg", 84);
hidden(lp); hidden(rp); hidden(lph); hidden(sval); hidden(ctitle);
hidden(pc1); hidden(pc2); hidden(pc3); hidden(pc4);
// ---- SCENE 1 motion: sweep s; the three left-panel graphs reshape together ----
wait(0.6);
par {
to(sfreq, value, -0.3, 4.5, smooth);
grow(svec, (129, 587), 4.5, smooth);
}
wait(0.5);
// ---- TRANSITION: the Laplace board fades, the panel becomes Euler's identity ----
par {
fade(ttl, 0.7); fade(fwd, 0.7); fade(inv, 0.7); fade(br, 0.7); fade(creature, 0.7);
fade(pL, 0.7); // the Scene 1 left panel — replaced by the framed Euler board
fade(pM, 0.7); fade(surf, 0.7); fade(fs, 0.7); fade(rlab, 0.7); fade(rose, 0.7); fade(ilab, 0.7); fade(iarc, 0.7);
fade(pR, 0.7); fade(cpl, 0.7); fade(contour, 0.7); fade(key, 0.7); fade(keyl, 0.7);
fade(wave, 0.7); fade(spA, 0.7); fade(svec, 0.7); fade(spir, 0.7); fade(rel, 0.7); fade(est, 0.7);
}
hidden(sfreq.widget);
// EMERGENCE (continuity — motion, not a hard cut): e^{πi}=-1 surfaces and the
// red π RISES up into the unit circle, leading the eye into the next scene.
show(eeuler, 0.7);
show(epi, 0.5);
par {
move(epi, (250, 354), 1.1, smooth); // π moves up to the circle top — the emergence cue
show(ecirc, 0.9);
show(earc, 1.0);
}
wait(0.3);
show(eboard, 0.6); // reveal the left panel's frame (aligned with the grid)
par { show(eax, 0.5); show(ecurve, 0.9); show(erad, 0.5); show(etan, 0.9); show(edl, 0.6); show(etri, 0.9); show(erise, 0.6); }
attach(edl, etan, (58, -14)); // the d/dt e^t=e^t label RIDES the tangent as it slides
attach(erise, etri.rise, (14, -20)); // the red e^t rise label rides the rise leg
wait(0.4);
show(eisq, 0.6);
wait(1.0);
// ---- SWEEP (00:05→00:17): the eye is carried from the identity into the grid ----
// Continuity again (motion-graphics.md): a teal arrow SWEEPS from the board over to
// the right, and the 5×5 of oscillators emerges under its tip — not a hard cut.
draw(swp, 1.0, smooth); // the teal arc sweeps board → grid, leading the eye
wait(0.2);
par {
show(phz.chrome, 0.5);
show(phz.headers, 0.6);
}
show(phz.cells, 0.9); // the phase grid surfaces under the arrow tip
// the grid runs AND the left board keeps moving — the tangent slides along e^t
par {
run(phz, 9); // all 25 oscillators run in sync
to(etan, x, 2.0, 8.5, smooth); // the e^t tangent slides (the left "slider" moves too)
to(etri, x, 2.0, 8.5, smooth); // ...and the slope triangle (run 1 + red rise e^t) slides with it
}
wait(0.4);
// ---- SCENE 4 (00:17→00:23): cut to the exponential e^{st}, then sweep the rate s ----
par { fade(euler, 0.6); fade(etri, 0.6); fade(phz, 0.6); fade(swp, 0.6); }
par { show(gax, 0.6); show(gcurve, 0.9); show(gyl, 0.5); show(gxl, 0.5); show(gte, 0.6); show(gsc, 0.6); show(gtt, 0.6); }
to(gs, value, -0.95, 2.4, smooth); // deepen the decay (curve steepens down-right)
to(gs, value, 0.5, 3.6, smooth); // through s=0 (flat) into growth (curve turns up)
wait(0.5);
// ---- SCENE 5 (00:24→00:37): make s symbolic + give it a number line, then slide s ----
// numeric title e^{0.50 t} → a single crisp symbolic equation e^{s t}
par { fade(gte, 0.5); fade(gsc, 0.5); fade(gtt, 0.5); show(esym, 0.6); }
// the s number line appears on the left (the graph already sits on the right)
par { show(sl, 0.6); show(slbox, 0.6); show(sread, 0.6); show(smk, 0.6); }
wait(0.6);
// slide s along the line — the gold marker, the "s =" readout, and the graph all track it
par { to(gs, value, -0.4, 2.2, smooth); move(smk, (209, 400), 2.2, smooth); }
par { to(gs, value, -1.0, 2.0, smooth); move(smk, (163, 400), 2.0, smooth); }
par { to(gs, value, -0.5, 2.0, smooth); move(smk, (201, 400), 2.0, smooth); }
wait(0.4);
// oscillate s between −0.5 and +0.5 a few times so the decay↔growth reshape reads clearly
par { to(gs, value, 0.5, 1.3, smooth); move(smk, (279, 400), 1.3, smooth); }
par { to(gs, value, -0.5, 1.3, smooth); move(smk, (201, 400), 1.3, smooth); }
par { to(gs, value, 0.5, 1.3, smooth); move(smk, (279, 400), 1.3, smooth); }
par { to(gs, value, -0.5, 1.3, smooth); move(smk, (201, 400), 1.3, smooth); }
par { to(gs, value, 0.5, 1.3, smooth); move(smk, (279, 400), 1.3, smooth); }
par { to(gs, value, -0.5, 1.3, smooth); move(smk, (201, 400), 1.3, smooth); }
wait(0.6);
// ---- SCENE 6 (00:37→00:53): the complex-plane payoff — with CONTINUITY, not a cut ----
// "Why" — the dramatic pivot (00:43): the scene is about to change under this one word.
show(whyt, 0.6);
wait(0.5);
// CONTINUITY (motion-graphics.md): the number line BECOMES the real axis of the complex plane
// (same centre + unit), so the real axis persists while the imaginary dimension grows around it.
par { fade(slbox, 0.6); fade(sread, 0.6); fade(graph, 0.6); }
show(lp, 0.9); // the complex plane grows in — its real axis = the number line
fade(sl, 0.7); // the bare number line hands its axis over to the plane
show(lph, 0.5);
move(smk, (224, 322), 1.4, smooth); // the s-dot LIFTS off ℝ up into ℂ — one object, transformed
show(sval, 0.6);
// the e^{st} title TRAVELS from top-left to head the right plane, which grows + traces the spiral
par { move(esym, (930, 210), 1.1, smooth); show(rp, 0.9); }
draw(spir6, 4.5, smooth); // e^{st} spirals: rotation from Im s × decay from Re s
wait(0.3);
par { fade(whyt, 0.5); show(ctitle, 0.7); } // "Why" → "Wait, what does this even mean?"
// the π-creatures pop up to watch (the 3b1b button)
par { show(pc1, 0.5); show(pc2, 0.6); show(pc3, 0.6); show(pc4, 0.5); }
wait(1.0);
lorenz-attractor
The classic strange attractor, drawn by trajectory3 — a 3D ODE integrated with RK4 into a single
stateful path (each point depends on every step before it, so it’s NOT a memoryless cloud),
speed-hued and orbited by the camera. σ=10, ρ=28, β=8/3: the birthplace of the butterfly effect.
One generic builtin draws any attractor — Lorenz, Rössler, Aizawa — from its derivative formulas.
// lorenz-attractor — the classic strange attractor, drawn by the new
// `trajectory3`: a 3-D ODE integrated with RK4 into a single stateful path, hued
// by speed and orbited by the camera. This is NOT a `cloud` (those are pure
// f(i,t) fields with no memory); an attractor is a recurrence — each point
// depends on every step before it — which is exactly what `trajectory3` adds.
//
// The Lorenz system (Edward Lorenz, 1963): ẋ=σ(y−x), ẏ=x(ρ−z)−y, ż=xy−βz,
// with σ=10, ρ=28, β=8/3 — the birthplace of "the butterfly effect". Reproduced
// from a p5 art-tweet by @yuruyurau (https://x.com/yuruyurau); our own 3-D,
// speed-hued, annotated take.
//
// manic examples/lorenz-attractor.manic
title("The Lorenz attractor");
canvas("9:16");
template("black");
camera3((10, -10, 6), (0, 0, 0), 30, perspective);
// integrate the 3-D flow with RK4; coloured by speed automatically
trajectory3(butterfly,
"10*(y - x)", // ẋ = σ(y − x)
"x*(28 - z) - y", // ẏ = x(ρ − z) − y
"x*y - 2.667*z", // ż = xy − βz
(0.1, 0, 0), 13000);
untraced(butterfly);
// ---- textbook annotations ----
caption(head, "The Lorenz attractor", (540, 150), 40);
caption(sub, "deterministic chaos — one 3D flow", (540, 220), 24);
hidden(head);
hidden(sub);
equation(eq, (540, 1660),
`\dot x=\sigma(y{-}x),\;\; \dot y=x(\rho{-}z){-}y,\;\; \dot z=xy-\beta z`, 26);
caption(lab, "σ=10, ρ=28, β=8/3 — integrated by RK4", (540, 1746), 22);
hidden(eq);
hidden(lab);
// ---- run it: the path draws on while the camera orbits, annotations reveal ----
par {
seq {
orbit3(45, 26, 11, 0.05, linear); // snap to a start view
orbit3(405, 26, 11, 22, linear); // then a full slow revolution
}
draw(butterfly, 12); // the attractor traces itself on
seq {
wait(0.6);
show(head);
wait(1.6);
show(sub);
wait(2.0);
show(eq);
show(lab);
wait(25);
}
}
cloud3-sphere
The 3D twin of cloud: 2500 points placed by closed-form x/y/z formulas of index i and time
t, projected by the orbit camera3. A golden-angle spiral spreads them over a sphere that
breathes and rotates, each point cycling its own hue — the generic particle field, one dimension up.
// cloud3-sphere — the 3D twin of `cloud`: N points placed by closed-form x/y/z
// formulas of the index `i` and live time `t`, projected by the orbit `camera3`.
// A golden-angle spiral spreads 2500 points evenly over a sphere; the radius
// breathes and the whole shell slowly rotates, each point cycling its own hue.
// Same primitive philosophy as 2D `cloud`, one dimension up.
//
// manic examples/cloud3-sphere.manic
title("cloud3 — a breathing rainbow sphere");
canvas(800, 800);
template("black");
camera3((9, -9, 6), (0, 0, 0), 42, perspective);
cloud3(orb, 2500, #ffffff, 0.95) {
let phi = i * 2.39996; // golden angle around the axis
let ct = 1 - 2 * (i + 0.5) / 2500; // cos(theta): points spread evenly in z
let st = sqrt(1 - ct * ct); // sin(theta)
let rad = 3 + 0.5 * sin(t * 2 + i * 0.02); // the shell breathes
let x = rad * st * cos(phi + t * 0.4); // ...and slowly rotates
let y = rad * st * sin(phi + t * 0.4);
let z = rad * ct;
let r = 0.05;
let hue = mod(i * 0.18 + t * 24, 360);
}
wait(10);
cloud3-ripple
The manic answer to a raymarched Shadertoy (tssSDN): a 64x64 cloud3 grid whose HEIGHT is a
travelling wave radiating from a bouncing sphere — cells near the sphere bob most, red troughs rise
to gold crests. The original ray-marches an SDF box-grid per pixel (a per-pixel loop manic’s
closed-form fields can’t run); here the same OUTCOME is REAL 3D geometry — every point a pure
function of index and time, projected by camera3, seekable and recordable. Reimagined, not ported.
// cloud3-ripple — the manic answer to a raymarched Shadertoy. The original
// (https://www.shadertoy.com/view/tssSDN) ray-marches a grid of boxes whose
// heights ripple in a wave radiating from a bouncing sphere. Ray-marching (a
// per-pixel loop over an SDF scene) is out of scope for manic's closed-form
// fields — but the OUTCOME is pure 3D geometry, so we REIMAGINE it with `cloud3`:
// a 64×64 grid of points whose height is a travelling wave centred on a moving
// sphere, each point a closed-form function of its index and time. Real 3D,
// projected by `camera3`, seekable and recordable — no raymarch, no assets.
//
// manic examples/cloud3-ripple.manic
title("A rippling grid — the cloud3 answer to a raymarch");
canvas("16:9");
template("black");
// manic 3D is Z-up: the grid floor lies in the x-y plane and ripples UP in z.
camera3((11, -11, 8), (0, 0, 0.4), 40, perspective);
// the grid: each point bobs on a wave that radiates from the sphere's position
cloud3(grid, 4096, #ff3524, 1.0) {
let gx = mod(i, 64) / 63 - 0.5; // cell column 0..63 → -0.5..0.5
let gy = floor(i / 64) / 63 - 0.5; // cell row
let x = gx * 9; // world -4.5..4.5 (floor)
let y = gy * 9;
let sx = sin(t * 1.8) * 2.4; // the sphere's orbit, inlined
let sy = cos(t * 2.2) * 2.4;
let d = hypot(x - sx, y - sy); // in-plane distance to the sphere
let fall = 1 - smoothstep(0.0, 3.4, d); // near the sphere ⇒ bob more
let z = 1.35 * sin(rand(i) * 3.14 + t * 2.5 + d * 2.2) * fall; // HEIGHT: per-cell phase + wave
let r = 0.09;
let hue = mix(2.0, 46.0, clamp(z * 0.7 + 0.4, 0.0, 1.0)); // red troughs → gold crests
}
// the little sphere that drives the wave (a tiny cloud3 shell, orbiting)
cloud3(ball, 90, #fff3b0, 1.0) {
let phi = i * 2.39996;
let ct = 1.0 - 2.0 * (i + 0.5) / 90.0;
let st = sqrt(1.0 - ct * ct);
let bx = sin(t * 1.8) * 2.4;
let by = cos(t * 2.2) * 2.4;
let x = bx + 0.42 * st * cos(phi);
let y = by + 0.42 * st * sin(phi);
let z = 1.7 + 0.42 * ct;
let r = 0.06;
}
caption(head, "One wave, a moving source", (640, 68), 34);
caption(sub, "a cloud3 grid + a bouncing sphere — real 3D, no raymarch", (640, 126), 22);
hidden(head);
hidden(sub);
show(head);
wait(1.5);
show(sub);
wait(26);
fibonacci-sphere
A textbook lesson built on cloud3: how do you scatter N points evenly over a sphere? The
answer — even heights plus a golden-angle turn — is derived in typeset LaTeX beside the live 3D
result, so the picture and the maths are the same object. cloud3 + equation + camera3.
// fibonacci-sphere — an educational cloud3 lesson: how do you scatter N points
// *evenly* over a sphere? Naive latitude/longitude clumps at the poles; the fix
// is the golden angle. This builds the answer live — 2000 points placed by the
// exact closed-form spiral, projected in 3D, with the derivation typeset beside
// it in LaTeX. The picture and the maths are the same object, which is the point.
//
// manic examples/fibonacci-sphere.manic
title("The Fibonacci sphere");
canvas("16:9");
template("black");
camera3((10, -10, 6), (0, 0, 0), 40, perspective);
// --- headings (caption centres the row; text() would left-anchor and clip) --
caption(head, "Even points on a sphere", (640, 52), 38);
caption(ask, "even heights, then a golden-angle turn each step", (640, 108), 22);
hidden(head);
hidden(ask);
// --- the sphere: the Fibonacci spiral, breathing-free so it reads as a lesson
cloud3(orb, 2000, #ffffff, 0.97) {
let ct = 1 - 2 * (i + 0.5) / 2000; // z_i: cos(theta), evenly spaced in [-1,1]
let st = sqrt(1 - ct * ct); // r_i: ring radius at that height
let th = i * 2.39996; // theta_i: i * the golden angle
let x = 3 * st * cos(th + t * 0.25); // (slow spin so it reads as a solid)
let y = 3 * st * sin(th + t * 0.25);
let z = 3 * ct;
let r = 0.05;
let hue = mod(200 + i * 0.14, 360); // a cool→warm gradient down the spiral
}
untraced(orb);
// --- the derivation, typeset beside it ------------------------------------
equation(e1, (270, 300), `z_i = 1 - \dfrac{2i+1}{N}`, 34);
equation(e2, (270, 410), `r_i = \sqrt{1 - z_i^{2}}`, 34);
equation(e3, (270, 520), `\theta_i = i\,\varphi`, 34);
equation(e4, (300, 640), `\varphi = \pi(3-\sqrt5) \approx 137.5^\circ`, 30);
hidden(e1);
hidden(e2);
hidden(e3);
hidden(e4);
// --- lesson beats ---------------------------------------------------------
show(head);
wait(0.8);
show(ask);
wait(1.2);
draw(orb, 3); // the sphere grows in as the points appear
wait(0.6);
show(e1); // even heights
wait(1.6);
show(e2); // the ring radius at each height
wait(1.6);
show(e3); // turn a little each step...
wait(1.4);
show(e4); // ...by the golden angle: the never-repeating turn
wait(3.5);
wireframe-spheres
A drifting field of triangulated wireframe spheres, each breathe-ing with a per-index phase so the
field pulses like soap bubbles. finish3(id,"wire=1") renders any solid as bright see-through edges.
// wireframe-spheres — a drifting field of triangulated wireframe spheres, each
// BREATHING (radius oscillating) with a phase tied to its index, so the field
// pulses like a shoal of soap bubbles. The breathing is the 3-D echo of the 2-D
// `wheel-radial` wave; `finish3(id, "wire=1")` renders any solid as its bright
// triangulated wireframe with no filled faces (works on cube/prism/pyramid too).
//
// manic examples/wireframe-spheres.manic
canvas(1080, 1080);
template("black");
camera3((0, -15, 1.5), (0, 0, 0), 56);
let m = 66;
for i in 0..m {
let x = 9.5 * sin(i * 2.399 + 0.5);
let y = 8.5 * sin(i * 1.13 + 1.7);
let z = 6.5 * sin(i * 3.11 + 0.3);
let r = 0.5 + 0.85 * sin(i * 1.7) * sin(i * 1.7);
sphere3(s{i}, (x, y, z), r);
finish3(s{i}, "wire=1");
hue(s{i}, 360 * i / m); // a computed rainbow — hue works on 3D now
}
// every sphere breathes at once, but each with its OWN amplitude, period and
// phase (deterministic pseudo-random from the index — manic has no rand()), so
// the bubbles pulse by different amounts instead of in lockstep.
par {
for i in 0..m {
let ra = 0.5 + 0.5 * sin(i * 12.9 + 0.5); // 0..1 pseudo-random
let rb = 0.5 + 0.5 * sin(i * 7.7 + 2.3);
let rc = 0.5 + 0.5 * sin(i * 5.3 + 4.1);
let amp = 0.28 + 0.55 * ra * ra; // breath size: ~0.28..0.83
let per = 2.0 + 2.2 * rb; // breath rate: 2.0..4.2 s
breathe(s{i}, per, amp, rc, 14);
}
}
three-d-v2
The compact 3D V2 reference: frame a tagged craft, attach its parts, travel one persistent subject along a spatial route, deploy the rig with a rigid turn, and become the final blueprint — five creator words in one continuous scene.
// three-d-v2.manic — the five creator-first spatial motion words
title("3D V2 — Five Words for Spatial Stories");
canvas("16:9");
template("shorts");
watermark(manicMark, (126, 64), "Made With Manic");
creator(me, "@anish2good name=Manic_3D tagline=Spatial_stories yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social cta=Create_yours safe=clean");
socials(me);
camera3((10, -13, 8), (0, 0, 1), 42);
grid3(floor, (0, 0, 0), 6, 1); color(floor, dim); opacity(floor, 0.24);
// A route can be a line3, arrow3, or curve3.
curve3(route, "-4 + 8*t", "-2 + 3*sin(pi*t)", "0.8 + 3.2*sin(pi*t)", (0, 1));
color(route, cyan); thick(route, 0.035); untraced(route);
cube3(ship, (-4, -2, 0.8), (1.4, 1.0, 0.65)); color(ship, gold);
sphere3(sensor, (-4, -2, 1.65), 0.34); color(sensor, cyan);
cube3(leftWing, (-4, -3.15, 0.8), (0.18, 1.8, 0.10)); color(leftWing, magenta);
cube3(rightWing, (-4, -0.85, 0.8), (0.18, 1.8, 0.10)); color(rightWing, magenta);
tag(ship, spacecraft); tag(sensor, spacecraft);
tag(leftWing, spacecraft); tag(rightWing, spacecraft);
tag(leftWing, wings); tag(rightWing, wings);
// A hidden target is a blueprint. become3 keeps `ship` as the persistent id.
sphere3(observatoryBlueprint, (4, -2, 0.95), 0.88);
color(observatoryBlueprint, lime); hidden(observatoryBlueprint);
text(caption, (640, 622), "FRAME · FOLLOW · TRAVEL · TRANSFORM · TURN");
size(caption, 24); bold(caption); color(caption, fg); hidden(caption);
step("frame-the-route") {
par {
view3(route, "fit", 1.0, smooth, 1.30);
show(caption, 0.45);
}
}
step("travel-as-one-system") {
attach3(sensor, ship, (0, 0, 0.85));
attach3(leftWing, ship, (0, -1.15, 0));
attach3(rightWing, ship, (0, 1.15, 0));
par {
draw(route, 2.8, smooth);
travel3(ship, route, 2.8, smooth);
say(caption, "attach3 + travel3", 0.55, smooth);
}
}
step("deploy-and-transform") {
seq {
attach3(sensor, none);
attach3(leftWing, none);
attach3(rightWing, none);
par {
turn3(wings, ship, z, 90, 1.1, smooth);
become3(ship, observatoryBlueprint, 1.1, smooth);
say(caption, "turn3 + become3", 0.55, smooth);
}
par {
view3(spacecraft, "isometric", 1.2, smooth, 1.35);
say(caption, "view3 settles the final composition", 0.55, smooth);
fade(route, 0.65);
}
wait(1.2);
}
}
three-d-v2-story
A vertical creator story about a satellite finding orbit. The same mission survives assembly, launch, transformation, deployment, and screen-aware camera composition without a scene reset.
// three-d-v2-story.manic — a vertical creator story built on 3D V2
title("From Signal to Satellite — A 3D Motion Story");
canvas("9:16");
template("shorts");
watermark(manicMark, (164, 86), "Made With Manic");
creator(me, "@anish2good name=Manic_3D tagline=Make_space_understandable yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social cta=Create_yours safe=reels");
socials(me);
text(kicker, (540, 164), "A 3D MOTION STORY");
size(kicker, 22); bold(kicker); color(kicker, cyan); hidden(kicker);
text(headline, (540, 226), "How a satellite finds its orbit");
size(headline, 39); bold(headline); color(headline, fg); hidden(headline);
text(chapter, (540, 300), "ACT 1 · BUILD ONE SYSTEM");
size(chapter, 23); bold(chapter); color(chapter, gold); hidden(chapter);
text(narration, (540, 1472), "Three parts. One mission.");
size(narration, 30); bold(narration); color(narration, fg); hidden(narration);
text(principle, (540, 1544), "Spatial motion stays readable when relationships survive the move.");
size(principle, 22); color(principle, dim); wrap(principle, 820); hidden(principle);
camera3((10, -14, 8), (0, 0, 1), 42);
grid3(floor, (0, 0, 0), 7, 1); color(floor, dim); opacity(floor, 0.20);
curve3(launchRoute, "-4 + 7*t", "-2 + 2.6*sin(pi*t)", "0.9 + 4.1*sin(pi*t)", (0, 1));
color(launchRoute, cyan); thick(launchRoute, 0.045); untraced(launchRoute);
cube3(core, (-4, -2, 0.9), (1.25, 0.95, 0.72)); color(core, gold); hidden(core);
sphere3(lens, (-4, -2, 1.78), 0.34); color(lens, cyan); hidden(lens);
cube3(panelA, (-4, -3.30, 0.9), (0.18, 2.1, 0.10)); color(panelA, magenta); hidden(panelA);
cube3(panelB, (-4, -0.70, 0.9), (0.18, 2.1, 0.10)); color(panelB, magenta); hidden(panelB);
tag(core, mission); tag(lens, mission); tag(panelA, mission); tag(panelB, mission);
tag(panelA, solarArray); tag(panelB, solarArray);
// The final observatory is only a blueprint. It never appears as a second object.
sphere3(observatory, (3, -2, 1.05), 0.86); color(observatory, lime); hidden(observatory);
text(lensLabel, (0, 0), "signal"); size(lensLabel, 21); color(lensLabel, cyan); hidden(lensLabel);
pin3(lensLabel, lens);
step("assemble") {
seq {
par {
show(kicker, 0.35);
show(headline, 0.50);
show(chapter, 0.35);
show(narration, 0.40);
show(principle, 0.40);
view3(mission, "isometric", 1.0, smooth, 1.55);
}
stagger(0.12) {
show(core, 0.35);
show(lens, 0.35);
show(panelA, 0.35);
show(panelB, 0.35);
}
attach3(lens, core, (0, 0, 0.88));
attach3(panelA, core, (0, -1.30, 0));
attach3(panelB, core, (0, 1.30, 0));
show(lensLabel, 0.30);
wait(0.45);
}
}
step("launch") {
seq {
par {
say(chapter, "ACT 2 · MOVE THE RELATIONSHIP", 0.45, smooth);
say(narration, "The craft travels. Every attached part remembers where it belongs.", 0.55, smooth);
view3(launchRoute, "fit", 1.1, smooth, 1.48);
}
par {
draw(launchRoute, 3.2, smooth);
travel3(core, launchRoute, 3.2, smooth);
say(principle, "attach3 carries context · travel3 carries identity", 0.55, smooth);
}
wait(0.45);
}
}
step("deploy") {
seq {
attach3(lens, none);
attach3(panelA, none);
attach3(panelB, none);
par {
say(chapter, "ACT 3 · DEPLOY THE IDEA", 0.45, smooth);
say(narration, "At orbit, only the changing parts move.", 0.50, smooth);
turn3(solarArray, core, z, 90, 1.25, smooth);
become3(core, observatory, 1.25, smooth);
}
par {
view3(mission, "isometric", 1.4, smooth, 1.58);
say(principle, "turn3 preserves the rig · become3 preserves the subject", 0.55, smooth);
fade(launchRoute, 0.65);
}
par {
view3(mission, "front", 2.2, smooth, 1.24);
pulse(lens, 0.75);
say(narration, "One persistent object. One continuous story.", 0.55, smooth);
}
wait(1.8);
}
}
three-d-v2-lab
A creator-first spatial lab: safe-aware framing, rigid assembly, a live projection and edge, a moving route, surface contour, depth-scaled label, bounded finishes, variable tube, and controlled OBJ geometry in one continuous story.
canvas(1080, 1920);
template("neon");
title("3D relationships, not keyframes");
creator(me, "@anish2good name=Manic yt=zarigatongy x=@anish2good web=8gwifi.org/manic footer=social safe=reels");
socials(me);
watermark(mark, (180, 88), "Made With Manic");
camera3((8, -10, 7), (0, 0, 1), 42);
grid3(floor, (0, 0, 0), 4, 1); color(floor, dim); opacity(floor, 0.28);
text(kicker, (540, 174), "SPATIAL STORY"); size(kicker, 22); color(kicker, cyan);
text(headline, (540, 242), "One point. Three relationships."); size(headline, 46);
text(caption, (540, 1350), "Move the idea — the construction follows."); size(caption, 27); color(caption, dim);
surface3(bowl, "0.16*(x^2+y^2)", (-3,3), (-3,3), 24);
color(bowl, blue); opacity(bowl, 0.42);
finish3(bowl, "shading=smooth mesh=0.18 depth=0.25 shadow=0.15");
contour3(level, bowl, 0.7); color(level, gold); thick(level, 0.025);
model3(beacon, "asset:models/manic-pyramid.obj", (-1.5, -0.4, 0.05), 0.8);
color(beacon, magenta);
finish3(beacon, "material=metal shading=flat mesh=0.20 depth=0.18 shadow=0.30");
point3(probe, (0, 0, 0), 0.15); color(probe, lime);
attach3(probe, beacon, (0, 0, 2.0), rigid);
project3(shadow, probe, "xy"); color(shadow, cyan);
link3(drop, probe, shadow, 0.12); color(drop, cyan); thick(drop, 0.018);
text(probeLabel, (0,0), "live point"); size(probeLabel, 28); color(probeLabel, lime);
label3(probeLabel, probe, 0.34);
curve3(route, "3*cos(pi*t)", "2.4*sin(pi*t)", "0.8+1.4*t", (0,1));
color(route, gold); untraced(route);
curve3(spine, "-2.8+1.2*t", "-2.2", "0.15+1.8*t", (0,1)); hidden(spine);
tube3(signal, spine, "0.025+0.075*t", 10); color(signal, gold); opacity(signal, 0.58);
finish3(signal, "material=metal shading=smooth depth=0.20"); hidden(signal);
tag(bowl, lab); tag(level, lab); tag(beacon, lab); tag(probe, lab);
tag(shadow, lab); tag(drop, lab); tag(route, lab); tag(signal, lab);
tag(spine, lab);
step("frame the safe media area") {
par {
show(kicker, 0.35);
show(headline, 0.55);
show(caption, 0.45);
view3(lab, "isometric", 1.1, smooth, 1.55);
}
wait(0.5);
}
step("rigid attachment") {
par {
turn3(beacon, beacon, z, 110, 1.5, smooth);
orbit3(42, 28, 10.8, 1.5, smooth);
}
wait(0.35);
}
step("live projection") {
par {
shift3(beacon, (2.4, 0.8, 0.3), 1.5, smooth);
look3((0.4, 0.2, 1.1), 1.5, smooth);
}
wait(0.35);
}
step("transformed path travel") {
attach3(probe, none);
par {
draw(route, 2.2, smooth);
travel3(probe, route, 2.2, smooth);
rotate3(route, (0, 0, 24), 2.2, smooth);
show(signal, 0.55);
}
wait(0.7);
}
step("one bounded finish") {
flash(level, gold);
view3(lab, "fit", 1.0, smooth, 1.55);
wait(1.0);
}
creator-coordinate-worlds
Solve the classic 3 × 4 × 12 cuboid diagonal as a complete creator story: isolate the 3 × 4 floor in native 2D, reuse its 5-unit diagonal in textbook-projected 3D, derive the 13-unit answer, then orbit the same retained frame3 in spatial 3D.
// A complete textbook problem told with three truthful presentations:
// 2D isolates the useful right triangle, textbook 3D builds the cuboid cleanly,
// and spatial 3D confirms that the final segment really crosses the volume.
title("The 3–4–12 Box — Find the Space Diagonal");
canvas("9:16");
template("paper");
creator(me,"@anish2good name=Manic_Geometry tagline=Textbooks_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact safe=clean");
socials(me);
watermark(manicMark,(w*0.17,h*0.065),"Made With Manic");
endcard(me,"title=Make_Math_Move cta=8gwifi.org/manic");
let u = (w+h-abs(w-h))/2160;
text(kicker,(cx,h*0.075),"MANIC · PYTHAGORAS IN 3D"); size(kicker,22*u); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline,(cx,h*0.13),"A box is 3 × 4 × 12. Find its space diagonal."); size(headline,36*u); bold(headline); wrap(headline,w*0.82); hidden(headline);
text(chapter,(cx,h*0.205),"THE PROBLEM"); size(chapter,23*u); bold(chapter); color(chapter,gold); hidden(chapter);
text(caption,(cx,h*0.805),"Connect one corner to the opposite corner—through the box."); size(caption,24*u); bold(caption); wrap(caption,w*0.78); hidden(caption);
text(note,(cx,h*0.855),"Hint: solve one right triangle at a time."); size(note,21*u); color(note,dim); wrap(note,w*0.78); hidden(note);
equation(work,(cx,h*0.735),`D=?`,43*u); hidden(work);
// ---------------------------------------------------------------------------
// 2D: isolate the 3 × 4 floor. This is the truthful view for the first triangle.
// ---------------------------------------------------------------------------
let lx = w*0.24;
let rx = w*0.76;
let ty = h*0.36;
let by = h*0.60;
line(floorBottom,(lx,by),(rx,by)); color(floorBottom,cyan); stroke(floorBottom,4*u); untraced(floorBottom); hidden(floorBottom); tag(floorBottom,floor2d);
line(floorRight,(rx,by),(rx,ty)); color(floorRight,magenta); stroke(floorRight,4*u); untraced(floorRight); hidden(floorRight); tag(floorRight,floor2d);
line(floorTop,(rx,ty),(lx,ty)); color(floorTop,cyan); stroke(floorTop,4*u); untraced(floorTop); hidden(floorTop); tag(floorTop,floor2d);
line(floorLeft,(lx,ty),(lx,by)); color(floorLeft,magenta); stroke(floorLeft,4*u); untraced(floorLeft); hidden(floorLeft); tag(floorLeft,floor2d);
line(floorDiagonal2,(lx,by),(rx,ty)); color(floorDiagonal2,gold); stroke(floorDiagonal2,6*u); untraced(floorDiagonal2); hidden(floorDiagonal2); tag(floorDiagonal2,floor2d);
dot(A2,(lx,by),9*u); filled(A2); color(A2,fg); hidden(A2); tag(A2,floor2d);
dot(C2,(rx,ty),9*u); filled(C2); color(C2,fg); hidden(C2); tag(C2,floor2d);
text(a2,(lx-22*u,by+24*u),"A"); size(a2,22*u); bold(a2); hidden(a2); tag(a2,floor2d);
text(c2,(rx+22*u,ty-20*u),"C"); size(c2,22*u); bold(c2); hidden(c2); tag(c2,floor2d);
text(side4,(cx,by+32*u),"4"); size(side4,24*u); bold(side4); color(side4,cyan); hidden(side4); tag(side4,floor2d);
text(side3,(rx+30*u,(ty+by)/2),"3"); size(side3,24*u); bold(side3); color(side3,magenta); hidden(side3); tag(side3,floor2d);
text(base5,(cx+22*u,(ty+by)/2-18*u),"5"); size(base5,25*u); bold(base5); color(base5,gold); hidden(base5); tag(base5,floor2d);
// ---------------------------------------------------------------------------
// 3D: the scientific frame retains the real 0..4, 0..3, 0..12 coordinates.
// Its display cube is intentionally balanced; the numbered axes keep the data
// honest while the space-diagonal geometry stays readable on a phone.
// ---------------------------------------------------------------------------
camera3((9,-11,8),(0,0,0),17,orthographic);
frame3(boxFrame,(0,0,0),(6,6,6),"x=0..4 y=0..3 z=0..12 xmajor=1 ymajor=1 zmajor=4 planes=xy:min,xz:min,yz:min mode=textbook");
hidden(boxFrame);
// Data-to-display corners of frame3:
// (0,0,0) -> (-3,-3,-3), (4,3,0) -> (3,3,-3), (4,3,12) -> (3,3,3).
point3(A3,(-3,-3,-3),0.15); color(A3,fg); hidden(A3); tag(A3,boxStory);
point3(C3,(3,3,-3),0.15); color(C3,gold); hidden(C3); tag(C3,boxStory);
point3(B3,(3,3,3),0.18); color(B3,lime); hidden(B3); tag(B3,boxStory);
line3(floorDiagonal3,(-3,-3,-3),(3,3,-3)); color(floorDiagonal3,gold); thick(floorDiagonal3,0.075); untraced(floorDiagonal3); hidden(floorDiagonal3); tag(floorDiagonal3,boxStory);
line3(height3,(3,3,-3),(3,3,3)); color(height3,magenta); thick(height3,0.075); untraced(height3); hidden(height3); tag(height3,boxStory);
line3(spaceDiagonal,(-3,-3,-3),(3,3,3)); color(spaceDiagonal,lime); thick(spaceDiagonal,0.105); untraced(spaceDiagonal); hidden(spaceDiagonal); tag(spaceDiagonal,boxStory);
text(a3,(0,0),"A"); size(a3,24*u); bold(a3); hidden(a3); pin3(a3,A3,(-18,16)); tag(a3,boxStory);
text(c3,(0,0),"C"); size(c3,24*u); bold(c3); color(c3,gold); hidden(c3); pin3(c3,C3,(16,14)); tag(c3,boxStory);
text(b3,(0,0),"B"); size(b3,24*u); bold(b3); color(b3,lime); hidden(b3); pin3(b3,B3,(16,-14)); tag(b3,boxStory);
point3(floorMid,(0,0,-3),0.01); hidden(floorMid);
point3(heightMid,(3,3,0),0.01); hidden(heightMid);
point3(spaceMid,(0,0,0),0.01); hidden(spaceMid);
text(floorLabel,(0,0),"AC = 5"); size(floorLabel,21*u); bold(floorLabel); color(floorLabel,gold); hidden(floorLabel); pin3(floorLabel,floorMid,(10,18)); tag(floorLabel,boxStory);
text(heightLabel,(0,0),"CB = 12"); size(heightLabel,21*u); bold(heightLabel); color(heightLabel,magenta); hidden(heightLabel); pin3(heightLabel,heightMid,(18,0)); tag(heightLabel,boxStory);
text(spaceLabel,(0,0),"AB = 13"); size(spaceLabel,22*u); bold(spaceLabel); color(spaceLabel,lime); hidden(spaceLabel); pin3(spaceLabel,spaceMid,(-18,-22)); tag(spaceLabel,boxStory);
// ---------------------------------------------------------------------------
// THE SOLUTION
// ---------------------------------------------------------------------------
step("pose the box problem") {
seq {
par { show(kicker,0.35); show(headline,0.50); show(chapter,0.40); show(caption,0.45); show(note,0.40); show(work,0.40); }
view3(boxFrame,"isometric",0.80,smooth,1.35);
show(boxFrame,0.75);
stagger(0.10) { show(A3,0.25); show(a3,0.25); show(B3,0.25); show(b3,0.25); }
par { show(spaceDiagonal,0.15); draw(spaceDiagonal,1.20,smooth); }
pulse(spaceDiagonal,0.65);
wait(0.60);
}
}
step("find the hidden floor diagonal") {
seq {
par {
fade(boxFrame,0.50);
fade(boxStory,0.50);
say(chapter,"1 · SOLVE THE 2D FLOOR",0.40,smooth);
say(caption,"The floor is a 3 × 4 rectangle. Its diagonal is the first hypotenuse.",0.50,smooth);
say(note,"This part is planar—so 2D is the clearest explanation.",0.45,smooth);
rewrite(work,`AC^2=3^2+4^2`,0.65,smooth);
}
stagger(0.10) {
show(floorBottom,0.10); draw(floorBottom,0.55,smooth);
show(floorRight,0.10); draw(floorRight,0.45,smooth);
show(floorTop,0.10); draw(floorTop,0.55,smooth);
show(floorLeft,0.10); draw(floorLeft,0.45,smooth);
show(A2,0.20); show(a2,0.20); show(C2,0.20); show(c2,0.20);
show(side4,0.25); show(side3,0.25);
}
par {
show(floorDiagonal2,0.10);
draw(floorDiagonal2,1.10,smooth);
rewrite(work,`AC=\sqrt{3^2+4^2}=\sqrt{25}=\textcolor{gold}{5}`,1.15,smooth);
}
show(base5,0.30);
pulse(floorDiagonal2,0.65);
wait(0.65);
}
}
step("use that answer inside the box") {
seq {
par {
fade(floor2d,0.50);
say(chapter,"2 · BUILD THE 3D RIGHT TRIANGLE",0.40,smooth);
say(caption,"Now AC = 5 meets the 12-unit height at a right angle.",0.50,smooth);
say(note,"The same coordinates return as a clean textbook projection.",0.45,smooth);
rewrite(work,`AB^2=AC^2+CB^2`,0.70,smooth);
}
view3(boxFrame,"isometric",0.75,smooth,1.35);
show(boxFrame,0.70);
stagger(0.10) {
show(A3,0.20); show(a3,0.20);
show(C3,0.20); show(c3,0.20);
show(floorDiagonal3,0.10); draw(floorDiagonal3,0.90,smooth); show(floorLabel,0.25);
show(height3,0.10); draw(height3,0.90,smooth); show(heightLabel,0.25);
show(B3,0.20); show(b3,0.20);
}
wait(0.35);
}
}
step("solve the space diagonal") {
seq {
par {
show(spaceDiagonal,0.10);
draw(spaceDiagonal,1.25,smooth);
rewrite(work,`AB=\sqrt{\textcolor{gold}{5^2}+\textcolor{magenta}{12^2}}`,0.95,smooth);
say(caption,"Pythagoras works again—this time across the volume.",0.45,smooth);
}
rewrite(work,`AB=\sqrt{25+144}=\sqrt{169}`,0.85,smooth);
par {
rewrite(work,`\boxed{AB=\textcolor{lime}{13}}`,0.75,smooth);
show(spaceLabel,0.30);
pulse(spaceDiagonal,0.80);
say(note,"Two right triangles · one exact answer",0.40,smooth);
}
wait(0.80);
}
}
step("see why the answer is spatial") {
seq {
par {
present3(boxFrame,spatial,0.70,smooth);
say(chapter,"3 · REVEAL THE SPATIAL TRUTH",0.40,smooth);
say(caption,"Orbit the same construction: AB crosses all three dimensions.",0.50,smooth);
say(note,"2D to calculate · textbook 3D to explain · spatial 3D to understand",0.50,smooth);
}
par { orbit3(48,26,12,1.80,smooth); pulse(spaceDiagonal,0.70); }
par { orbit3(132,34,12,1.80,smooth); pulse(boxFrame.axes,0.70); }
wait(0.85);
}
}
step("creator call to action") {
par {
fade(boxFrame,0.45);
fade(boxStory,0.45);
fade(work,0.35);
fade(chapter,0.35);
fade(caption,0.35);
fade(note,0.35);
show(me.endcard,0.60);
}
wait(1.80);
}
creator-point-to-plane-distance
A pure-3D vector-geometry Short: read the normal of x+y+z=3, project P=(2,2,5) onto the plane, construct the spatial right angle, derive the exact distance 2√3, and orbit the persistent point-plane proof.
// A pure-3D textbook problem. Every geometric object stays spatial:
// a point, a plane, its normal, the perpendicular foot, and the shortest path.
title("Shortest Distance from a Point to a Plane");
canvas("9:16");
template("neon");
creator(me,"@anish2good name=Manic_Geometry tagline=3D_math_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact safe=clean");
socials(me);
watermark(manicMark,(w*0.17,h*0.065),"Made With Manic");
endcard(me,"title=Make_3D_Math_Move cta=8gwifi.org/manic");
let u = (w+h-abs(w-h))/2160;
text(kicker,(cx,h*0.075),"MANIC · VECTOR GEOMETRY"); size(kicker,22*u); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline,(cx,h*0.125),"How far is a point from a plane?"); size(headline,38*u); bold(headline); wrap(headline,w*0.82); hidden(headline);
text(chapter,(cx,h*0.185),"THE SHORTEST PATH MUST BE PERPENDICULAR"); size(chapter,22*u); bold(chapter); color(chapter,gold); wrap(chapter,w*0.82); hidden(chapter);
equation(problem,(cx,h*0.235),`P=(2,2,5),\qquad \Pi:x+y+z=3`,36*u); hidden(problem);
equation(work,(cx,h*0.745),`d(P,\Pi)=?`,43*u); hidden(work);
text(caption,(cx,h*0.815),"A slanted route is longer. The normal reveals the minimum."); size(caption,24*u); bold(caption); wrap(caption,w*0.80); hidden(caption);
text(note,(cx,h*0.86),"Find the direction · locate the foot · measure the segment"); size(note,21*u); color(note,dim); wrap(note,w*0.80); hidden(note);
// The frame uses the same physical coordinates as the problem:
// x,y in [-3,3] and z in [0,6] map one-to-one into a 6-unit display cube.
// A perspective three-quarter view balances both facts: the plane reads as a
// full surface, while its perpendicular keeps enough screen length to remain
// obvious. This story never switches to an orthographic/textbook projection.
camera3((20,-5,17),(0,0,2.8),42);
frame3(world,(0,0,3),(6,6,6),"x=-3..3 y=-3..3 z=0..6 xmajor=1 ymajor=1 zmajor=2 planes=xy:min mode=spatial");
hidden(world);
// Pi: x+y+z=3, rendered as z=3-x-y over a bounded patch.
surface3(plane,"3-x-y",(-1.5,1.5),(-1.5,1.5),20);
color(plane,cyan);
finish3(plane,"material=glass shading=flat mesh=0.38 depth=0.32 shadow=0.12");
hidden(plane);
// Crisp boundary keeps the translucent plane readable on a phone.
line3(edge0,(-1.5,-1.5,6),(1.5,-1.5,3)); color(edge0,cyan); thick(edge0,0.045); untraced(edge0); hidden(edge0); tag(edge0,planeEdge);
line3(edge1,(1.5,-1.5,3),(1.5,1.5,0)); color(edge1,cyan); thick(edge1,0.045); untraced(edge1); hidden(edge1); tag(edge1,planeEdge);
line3(edge2,(1.5,1.5,0),(-1.5,1.5,3)); color(edge2,cyan); thick(edge2,0.045); untraced(edge2); hidden(edge2); tag(edge2,planeEdge);
line3(edge3,(-1.5,1.5,3),(-1.5,-1.5,6)); color(edge3,cyan); thick(edge3,0.045); untraced(edge3); hidden(edge3); tag(edge3,planeEdge);
// P is the given point. Q is its perpendicular projection onto Pi.
point3(P,(2,2,5),0.20); color(P,magenta); hidden(P); tag(P,geometry);
point3(Q,(0,0,3),0.18); color(Q,gold); hidden(Q); tag(Q,geometry);
text(pLabel,(0,0),"P (2, 2, 5)"); size(pLabel,22*u); bold(pLabel); color(pLabel,magenta); hidden(pLabel); pin3(pLabel,P,(20,-18)); tag(pLabel,geometry);
text(qLabel,(0,0),"Q (0, 0, 3)"); size(qLabel,22*u); bold(qLabel); color(qLabel,gold); hidden(qLabel); pin3(qLabel,Q,(-24,20)); tag(qLabel,geometry);
// The gradient of x+y+z-3 is n=(1,1,1).
arrow3(normal,(0,0,3),(1.15,1.15,4.15)); color(normal,gold); thick(normal,0.075); untraced(normal); hidden(normal);
point3(nTip,(1.15,1.15,4.15),0.01); hidden(nTip);
text(nLabel,(0,0),"n = (1, 1, 1)"); size(nLabel,21*u); bold(nLabel); color(nLabel,gold); hidden(nLabel); pin3(nLabel,nTip,(18,-14));
// The minimum-distance segment is parallel to n and ends at Q.
line3(distanceLine,(0,0,3),(2,2,5)); color(distanceLine,lime); thick(distanceLine,0.105); untraced(distanceLine); hidden(distanceLine); tag(distanceLine,geometry);
point3(distanceMid,(1,1,4),0.01); hidden(distanceMid);
text(distanceLabel,(0,0),"d = 2√3"); size(distanceLabel,23*u); bold(distanceLabel); color(distanceLabel,lime); hidden(distanceLabel); pin3(distanceLabel,distanceMid,(-22,-20)); tag(distanceLabel,geometry);
// A small spatial right-angle mark: one side lies in Pi, one follows n.
line3(right0,(0,0,3),(0.45,-0.45,3)); color(right0,fg); thick(right0,0.045); untraced(right0); hidden(right0); tag(right0,rightMark);
line3(right1,(0.45,-0.45,3),(0.70,-0.20,3.25)); color(right1,fg); thick(right1,0.045); untraced(right1); hidden(right1); tag(right1,rightMark);
line3(right2,(0.70,-0.20,3.25),(0.25,0.25,3.25)); color(right2,fg); thick(right2,0.045); untraced(right2); hidden(right2); tag(right2,rightMark);
step("pose the spatial problem") {
seq {
par {
show(kicker,0.35); show(headline,0.50); show(chapter,0.45);
show(problem,0.55); show(work,0.40); show(caption,0.45); show(note,0.40);
}
show(world,0.70);
par {
show(plane,0.75);
show(edge0,0.10); draw(edge0,0.70,smooth);
show(edge1,0.10); draw(edge1,0.70,smooth);
show(edge2,0.10); draw(edge2,0.70,smooth);
show(edge3,0.10); draw(edge3,0.70,smooth);
}
stagger(0.12) { show(P,0.25); show(pLabel,0.30); pulse(P,0.60); }
wait(0.65);
}
}
step("read the plane normal") {
seq {
par {
say(chapter,"1 · READ THE NORMAL FROM THE PLANE",0.40,smooth);
say(caption,"The coefficients of x, y, and z point perpendicular to Π.",0.50,smooth);
say(note,"For ax + by + cz + d = 0, the normal is (a,b,c).",0.45,smooth);
rewrite(work,`\Pi:x+y+z-3=0`,0.65,smooth);
fade(pLabel,0.30);
}
par {
show(Q,0.25);
show(normal,0.10); draw(normal,1.10,smooth);
show(nLabel,0.35);
rewrite(work,`\mathbf n=(1,1,1)`,0.80,smooth);
}
pulse(normal,0.65);
wait(0.60);
}
}
step("locate the perpendicular foot") {
seq {
par {
fade(normal,0.35); fade(nLabel,0.35);
say(chapter,"2 · DROP THE PERPENDICULAR",0.40,smooth);
say(caption,"Move from P opposite the normal until the plane equation becomes true.",0.50,smooth);
say(note,"Q = P − t n must lie on Π.",0.40,smooth);
rewrite(work,`Q=(2,2,5)-t(1,1,1)`,0.80,smooth);
}
rewrite(work,`(2-t)+(2-t)+(5-t)=3\Rightarrow t=2`,1.00,smooth);
par {
show(distanceLine,0.10); draw(distanceLine,1.20,smooth);
show(right0,0.10); draw(right0,0.55,smooth);
show(right1,0.10); draw(right1,0.45,smooth);
show(right2,0.10); draw(right2,0.55,smooth);
rewrite(work,`Q=(0,0,3)`,0.75,smooth);
}
pulse(Q,0.65);
wait(0.65);
}
}
step("measure the shortest segment") {
seq {
par {
say(chapter,"3 · MEASURE THE NORMAL SEGMENT",0.40,smooth);
say(caption,"The point-to-plane formula measures exactly the green segment PQ.",0.50,smooth);
say(note,"Absolute value gives distance; the denominator normalizes n.",0.45,smooth);
rewrite(work,`d=\frac{|2+2+5-3|}{\sqrt{1^2+1^2+1^2}}`,1.05,smooth);
}
rewrite(work,`d=\frac{6}{\sqrt3}=\textcolor{lime}{2\sqrt3}`,0.90,smooth);
par {
rewrite(work,`\boxed{d(P,\Pi)=\textcolor{lime}{2\sqrt3}}`,0.75,smooth);
show(distanceLabel,0.30);
pulse(distanceLine,0.80);
}
wait(0.75);
}
}
step("orbit the proof") {
seq {
par {
say(chapter,"THE ANSWER IS A SPATIAL RELATIONSHIP",0.45,smooth);
say(caption,"Orbit the proof: PQ stays normal to Π from every viewpoint.",0.50,smooth);
say(note,"One plane · one projection · one shortest path",0.45,smooth);
fade(pLabel,0.35); fade(qLabel,0.35);
}
par { orbit3(38,28,24,1.80,smooth); pulse(distanceLine,0.70); }
par { orbit3(68,30,24,1.80,smooth); pulse(plane,0.70); }
wait(0.80);
}
}
step("creator call to action") {
par {
fade(world,0.45); fade(plane,0.45); fade(planeEdge,0.45);
fade(geometry,0.45); fade(rightMark,0.45);
fade(problem,0.35); fade(work,0.35); fade(chapter,0.35);
fade(caption,0.35); fade(note,0.35);
show(me.endcard,0.60);
}
wait(1.80);
}
frame3-grid-policies
The six Asymptote-inspired scientific grid policies in one review: lower walls, a logarithmic axis, top and explicit planes, coloured parallel sections, major/minor grids, and independent per-axis intervals.
// Review file for the six Asymptote grid3 policies that motivated frame3.
// Each chapter is a normal frame3 declaration, not a renderer special case.
title("Scientific Grid Planes — Asymptote Parity");
canvas("16:9");
template("mono");
watermark(manicMark,(170,70),"Made With Manic");
text(titleText,(640,68),"One frame vocabulary · six grid policies"); size(titleText,30); bold(titleText);
text(caption,(640,670),"Three lower walls"); size(caption,24); bold(caption); color(caption,gold);
camera3((10,-12,8),(0,0,0),15,orthographic);
frame3(walls,(0,0,0),(8,8,6),"x=-2..2 y=-2..2 z=-1..3 planes=xy:min,xz:min,yz:min major=1 mode=textbook");
frame3(logWalls,(0,0,0),(8,8,6),"x=0..4 y=0..4 z=0.1..100 planes=xy:min,xz:min zscale=log major=1 mode=textbook");
frame3(positioned,(0,0,0),(8,8,6),"x=-2..2 y=-2..2 z=-2..2 planes=xy:max,xz:0 major=1 mode=textbook");
frame3(parallel,(0,0,0),(8,8,6),"x=-2..2 y=-2..2 z=-2..2 planes=xy:-0.5@cyan,xy:1.5@magenta major=1 mode=textbook");
frame3(majorMinor,(0,0,0),(8,8,6),"x=-2..2 y=-2..2 z=-1..2 planes=xy:min major=1 minor=0.5 mode=textbook");
frame3(axisPolicy,(0,0,0),(8,8,6),"x=-2..2 y=-2..2 z=-2..2 planes=xy:min,yz:min xmajor=0.5 ymajor=1 zmajor=2 mode=textbook");
hidden(logWalls); hidden(positioned); hidden(parallel); hidden(majorMinor); hidden(axisPolicy);
step("three lower walls") {
show(walls,0.65);
wait(0.70);
}
step("linear floor with logarithmic height") {
par { fade(walls,0.45); show(logWalls,0.60); say(caption,"Linear floor · logarithmic z",0.40,smooth); }
wait(0.70);
}
step("top and explicit-position planes") {
par { fade(logWalls,0.45); show(positioned,0.60); say(caption,"XY at max · XZ at y=0",0.40,smooth); }
wait(0.70);
}
step("parallel colored sections") {
par { fade(positioned,0.45); show(parallel,0.60); say(caption,"Two parallel XY sections",0.40,smooth); }
wait(0.70);
}
step("major and minor lines") {
par { fade(parallel,0.45); show(majorMinor,0.60); say(caption,"Major ticks derive the major grid",0.40,smooth); }
wait(0.70);
}
step("independent axis policy") {
par { fade(majorMinor,0.45); show(axisPolicy,0.60); say(caption,"Each axis chooses its own interval",0.40,smooth); }
present3(axisPolicy,spatial,0.70,smooth);
orbit3(118,30,12,2.10,smooth);
wait(1.40);
}
asymptote-randomwalk3-reference
The recurring Asymptote 3D random-walk families reduced to two readable models: world-axis choices and a local turtle frame with 90°/60° turns.
// The seven Asymptote randomwalk examples reduce to two reusable models:
// six world-axis choices, or a local 3D turtle that turns by 90°/60°.
title("Deterministic 3D Random Walks");
canvas("16:9");
template("mono");
watermark(mark, (w*0.11,h*0.075), "Made With Manic");
text(kicker, (cx,h*0.07), "GENERATIVE 3D · RANDOMWALK3");
text(headline, (cx,h*0.13), "The seed fixes the journey. The model changes its shape.");
text(caption, (cx,h*0.91), "Axis walk · uniform choices · direction colour · camera-aware depth");
size(kicker,19); bold(kicker); color(kicker,dim);
size(headline,32); bold(headline);
size(caption,20); bold(caption); color(caption,gold);
camera3((12,-15,10),(0,0,0),38,orthographic);
randomwalk3(axisWalk, (0,0,0), 12000, 21,
"mode=axis distribution=uniform color=direction shade=depth scale=0.12");
untraced(axisWalk);
randomwalk3(turtle90, (0,0,0), 9000, 21,
"mode=turtle angle=90 distribution=gaussian color=turn shade=depth scale=0.12");
untraced(turtle90); hidden(turtle90);
randomwalk3(turtle60, (0,0,0), 9000, 21,
"mode=turtle angle=60 distribution=uniform color=turn shade=depth scale=0.12");
untraced(turtle60); hidden(turtle60);
step("six world directions") {
par { draw(axisWalk,2.20,smooth); view3(axisWalk,"fit",0.60,smooth,1.20); }
}
wait(0.65);
step("the frame can turn with the walker") {
par {
fade(axisWalk,0.40);
show(turtle90,0.05);
say(caption,"Turtle frame · Gaussian choices · 90° turns",0.40);
}
par { draw(turtle90,2.20,smooth); view3(turtle90,"fit",0.60,smooth,1.20); }
}
wait(0.65);
step("change one angle") {
par {
fade(turtle90,0.40);
show(turtle60,0.05);
say(caption,"Turtle frame · uniform choices · 60° turns",0.40);
}
par { draw(turtle60,2.20,smooth); view3(turtle60,"fit",0.60,smooth,1.20); }
}
wait(0.70);
step("inspect the structure") {
orbit3(115,28,13,2.10,smooth);
}
wait(1.20);
creator-randomwalk3-diffusion
A creator Short grows the same seeded 3D walk from 100 to 10,000 decisions and reveals the large-scale shape hiding inside random local choices.
// Creator short: many random decisions do not create visual noise only—the
// choice model leaves a characteristic 3D signature.
title("Can Randomness Have a Shape?");
canvas("9:16");
template("paper");
creator(me, "@anish2good name=Manic_Generative tagline=Randomness_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Animate_the_invisible safe=clean");
socials(me);
watermark(mark, (w*0.16,h*0.055), "Made With Manic");
endcard(me, "title=Make_Invisible_Systems_Visible cta=8gwifi.org/manic");
let u = (w+h-abs(w-h))/1080;
text(kicker, (cx,h*0.11), "MANIC · RANDOM SYSTEMS");
text(headline, (cx,h*0.18), "Can randomness have a shape?");
text(caption, (cx,h*0.77), "Start at one point. Choose one of six directions.");
text(count, (cx,h*0.70), "100 STEPS");
size(kicker,20*u); bold(kicker); color(kicker,cyan);
size(headline,31*u); bold(headline); wrap(headline,w*0.78);
size(caption,23*u); bold(caption); wrap(caption,w*0.75);
size(count,20*u); bold(count); color(count,gold);
camera3((9,-12,8),(0,0,0),38,orthographic);
randomwalk3(w100,(0,0,0),100,42,"mode=axis color=direction scale=0.25");
randomwalk3(w1000,(0,0,0),1000,42,"mode=axis color=direction shade=depth scale=0.16");
randomwalk3(w10000,(0,0,0),10000,42,"mode=axis color=direction shade=depth scale=0.09");
untraced(w100); untraced(w1000); untraced(w10000);
hidden(w1000); hidden(w10000);
hidden(kicker); hidden(headline); hidden(caption); hidden(count);
step("one random journey") {
par {
show(kicker,0.35); show(headline,0.45); show(caption,0.40); show(count,0.35);
draw(w100,1.25,smooth); view3(w100,"fit",0.55,smooth,1.35);
}
}
wait(0.55);
step("the cloud begins to emerge") {
par {
fade(w100,0.35); show(w1000,0.05);
say(count,"1,000 STEPS",0.30);
say(caption,"More choices reveal a spreading 3D cloud.",0.40);
}
par { draw(w1000,1.75,smooth); view3(w1000,"fit",0.55,smooth,1.35); }
}
wait(0.60);
step("scale exposes the law") {
par {
fade(w1000,0.35); show(w10000,0.05);
say(count,"10,000 STEPS · SAME SEED",0.30);
say(caption,"The individual turns are random. The large-scale diffusion is not.",0.45);
}
par { draw(w10000,2.30,smooth); view3(w10000,"fit",0.60,smooth,1.35); }
orbit3(112,30,11,1.70,smooth);
}
wait(0.85);
step("creator takeaway") {
say(caption,"Manic turns a model and a seed into a camera-ready explanation.",0.45);
recolor(headline,gold,0.45);
}
wait(1.00);
step("call to action") {
par {
fade(kicker,0.30); fade(headline,0.30); fade(caption,0.30); fade(count,0.30);
fade(w10000,0.40); fade(me.footer,0.30); show(me.endcard,0.60);
}
}
wait(1.80);
parameterized-generated-families3
The compact generated-family reference: one ordinary parameter continuously changes a helix, height field, and torus while every object keeps its id, sample topology, material, and timeline identity.
// PARAMETERIZED GENERATED FAMILIES — one ordinary parameter, three stable 3-D
// objects. `p` changes sampled points; ids, materials, transforms, and sample
// topology stay intact. No per-vertex scripting or snapshot replacement.
title("Parameterized Generated Families");
canvas("16:9");
template("mono");
watermark(mark, (170, 55), "Made With Manic");
text(kicker, (640, 48), "MANIC · GENERATED 3-D FAMILIES");
text(headline, (640, 92), "One parameter · three continuous shapes");
text(caption, (640, 664), "curve3(t,p) · surface3(x,y,p) · param3(u,v,p)");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
size(caption, 19); color(caption, dim);
parameter(shape, (640, 612), 0, 0, 1, "shape", 2);
if h > 1.45*w {
camera3((0, -36, 8), (0, 0, 0.4), 40);
}
else {
camera3((10, -17, 9), (0, 0, 0.4), 40);
}
grid3(floor, (0, 0, -2.2), 8, 1);
color(floor, dim); opacity(floor, 0.14);
// A widening helix at the left.
curve3(helix,
"-4 + cos(t)",
"sin(t)",
"-1.7 + 0.55*t",
(0, 6.2832));
bind(shape, helix, formula,
"-4 + (1+0.45*p)*cos(t)",
"(1+0.45*p)*sin(t)",
"-1.7 + 0.55*t");
color(helix, cyan); thick(helix, 0.065); untraced(helix);
tag(helix, family);
// A bowl that becomes a rippling field in the middle.
surface3(land, "0.15*(x*x+y*y)", (-1.7,1.7), (-1.7,1.7), 28);
bind(shape, land, formula,
"(1-p)*0.15*(x*x+y*y) + p*0.72*sin(1.7*x)*cos(1.7*y)");
color(land, gold);
finish3(land, "material=glass shading=smooth mesh=0.30 depth=0.28 shadow=0.12");
tag(land, family);
// A torus whose tube inflates at the right.
param3(ring,
"4+(1.15+0.20*cos(v))*cos(u)",
"(1.15+0.20*cos(v))*sin(u)",
"0.20*sin(v)",
(0,6.2832), (0,6.2832), 28);
bind(shape, ring, formula,
"4+(1.15+(0.20+0.52*p)*cos(v))*cos(u)",
"(1.15+(0.20+0.52*p)*cos(v))*sin(u)",
"(0.20+0.52*p)*sin(v)");
color(ring, magenta);
finish3(ring, "material=metal shading=smooth mesh=0.22 depth=0.30 shadow=0.16");
tag(ring, family);
hidden(helix); hidden(land); hidden(ring);
step("reveal one stable family") {
par {
view3(family, "fit", 0.9, smooth, 1.55);
stagger(0.16) {
show(helix, 0.45);
show(land, 0.45);
show(ring, 0.45);
}
}
}
wait(0.45);
step("change the shared parameter") {
par {
to(shape, value, 1, 3.4, smooth);
say(caption, "The same three objects are resampled smoothly from p = 0 to p = 1.", 0.45);
}
}
wait(1.2);
creator-bowl-to-saddle
A vertical calculus Short asks when a bowl loses one direction of curvature. One persistent surface passes through the exact p=1/2 threshold and becomes a saddle, then closes with a creator CTA.
// CREATOR SHORT — a real calculus problem told through one continuously
// deforming surface. The shape is not replaced between the bowl, threshold,
// and saddle states.
title("When Does a Bowl Become a Saddle?");
canvas("9:16");
template("paper");
creator(me, "@anish2good name=Manic_Math tagline=Equations_that_move yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=signature cta=Create_your_own safe=reels");
socials(me);
watermark(mark, (w*0.955-100, h*0.045+24), "Made With Manic");
text(kicker, (540, 164), "A 20-SECOND CALCULUS STORY");
text(headline, (540, 226), "When does a bowl stop being a bowl?");
text(problem, (540, 292), "Find the exact value of p where one curvature disappears.");
size(kicker, 21); bold(kicker); color(kicker, cyan); hidden(kicker);
size(headline, 38); bold(headline); wrap(headline, 880); hidden(headline);
size(problem, 22); color(problem, dim); wrap(problem, 840); hidden(problem);
equation(rule, (540, 378), `z=0.22x^2+0.22(1-2p)y^2`, 39);
hidden(rule);
parameter(mix, (540, 1270), 0, 0, 1, "p", 2);
camera3((8.5, -11.5, 7.5), (0, 0, 0), 41);
surface3(world, "0.22*(x*x+y*y)", (-3,3), (-3,3), 34);
bind(mix, world, formula, "0.22*x*x + 0.22*(1-2*p)*y*y");
color(world, cyan);
finish3(world, "material=glass shading=smooth mesh=0.34 depth=0.34 shadow=0.16");
hidden(world);
text(chapter, (540, 1352), "p = 0 · positive curvature in both directions");
text(insight, (540, 1422), "Watch the coefficient of y².");
text(cta, (540, 1398), "Build the transformation, not the keyframes.");
text(link, (540, 1452), "TRY MANIC → 8gwifi.org/manic");
size(chapter, 23); bold(chapter); color(chapter, fg); wrap(chapter, 850); hidden(chapter);
size(insight, 21); color(insight, dim); hidden(insight);
size(cta, 23); bold(cta); hidden(cta);
size(link, 20); bold(link); color(link, cyan); hidden(link);
step("pose the problem") {
par {
show(kicker, 0.35);
show(headline, 0.50);
show(problem, 0.45);
show(rule, 0.55);
show(world, 0.65);
show(chapter, 0.40);
show(insight, 0.40);
view3(world, "isometric", 0.9, smooth, 1.62);
}
}
wait(0.65);
step("reach the turning point") {
par {
to(mix, value, 0.5, 2.8, smooth);
say(chapter, "p = ½ · the y² coefficient becomes zero", 0.45);
say(insight, "At this instant the bowl becomes a one-directional trough.", 0.45);
rewrite(rule, `p=\frac12\quad\Longrightarrow\quad z=0.22x^2`, 1.10, smooth);
orbit3(-45, 28, 16.2, 2.8, smooth);
}
}
wait(0.75);
step("cross into a saddle") {
par {
to(mix, value, 1, 2.8, smooth);
say(chapter, "p > ½ · one curvature turns negative", 0.45);
say(insight, "The same persistent surface is now a saddle.", 0.45);
rewrite(rule, `p=1\quad\Longrightarrow\quad z=0.22(x^2-y^2)`, 1.10, smooth);
orbit3(-62, 25, 16.2, 2.8, smooth);
}
}
wait(0.65);
step("answer and invite") {
par {
say(problem, "Answer: p = ½ is the exact transition.", 0.50);
fade(chapter, 0.35);
fade(insight, 0.35);
show(cta, 0.45);
show(link, 0.45);
}
}
wait(1.8);
story-living-dependency-cloud
A living dependency cloud grows, links itself by nearest neighbours, and drifts as one batched 3D cast. The relationships stay attached without hand-authoring dozens of nodes or links.
title("The Dependency Cloud That Refuses to Freeze");
canvas("9:16");
template("paper");
watermark(mark, (w*0.16, h*0.055), "Made With Manic");
text(kicker, (cx, h*0.085), "ENGINE FOUNDATION · BATCHED RELATIONSHIPS");
text(headline, (cx, h*0.13), "Can 180 dependencies move as one idea?");
text(caption, (cx, h*0.82), "One collection. Stable children. Every relationship stays truthful.");
size(kicker, 20); color(kicker, dim); bold(kicker); hidden(kicker);
size(headline, 34); bold(headline); wrap(headline, w*0.82); hidden(headline);
size(caption, 22); color(caption, dim); wrap(caption, w*0.78); hidden(caption);
camera3((14,-20,13),(0,0,0),46);
collection3(services,(0,0,0),180,(4.2,2.6,2.2),41,0.055);
links3(dependencies,services,nearest,2);
color(services,cyan);
color(dependencies,dim);
hidden(services);
hidden(dependencies);
step("the-problem") {
show(kicker,0.35);
show(headline,0.45);
show(caption,0.40);
}
wait(0.45);
step("the-system-awakens") {
par {
show(services,0.65);
show(dependencies,0.85);
say(caption,"The engine batches the repeated geometry but preserves every indexed identity.",0.45);
}
}
wait(0.35);
step("relationships-remain-live") {
par {
drift3(services,6.0,0.42);
orbit3(35,24,28.0,6.0,smooth);
say(caption,"As the nodes drift, the nearest-neighbour graph is derived again from the same state.",0.45);
}
}
wait(0.45);
step("takeaway") {
pulse(services,0.7);
say(caption,"Large dependency scenes become one creator intention—not hundreds of scripts.",0.45);
}
wait(1.2);
story-dependent-chain-history
An articulated signal arm is generated as one dependent chain: every endpoint begins at the previous endpoint, while the tip leaves a truthful history of its real route.
title("How Rotations Become a Drawing");
canvas("9:16");
template("blank");
watermark(mark, (w*0.16, h*0.055), "Made With Manic");
text(kicker, (cx, h*0.085), "ENGINE FOUNDATION · DEPENDENT CHAINS");
text(headline, (cx, h*0.13), "A drawing can hide inside rotating links");
text(caption, (cx, h*0.83), "Each endpoint begins where the previous endpoint finishes.");
size(kicker,20); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,34); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(caption,22); color(caption,dim); wrap(caption,w*0.80); hidden(caption);
camera3((0,-18,13),(0,0,0),46);
collection3(harmonics,(0,0,0),7,(0,0,0),7,0.10);
links3(arms,harmonics,chain);
trail3(memory,harmonics,6,0.035);
child3(tip,harmonics,6,0.14);
color(harmonics,cyan);
color(arms,magenta);
color(memory,gold);
color(tip,gold);
hidden(harmonics); hidden(arms); hidden(memory); hidden(tip);
step("question") {
show(kicker,0.35);
show(headline,0.45);
show(caption,0.40);
}
wait(0.35);
step("build-the-chain") {
par {
show(harmonics,0.55);
show(arms,0.65);
show(memory,0.45);
show(tip,0.45);
cue(tick);
say(caption,"Seven stable links share one generated dependency chain.",0.40);
}
}
wait(0.30);
step("remember-the-tip") {
par {
chain3(harmonics,"2.2 1.1 0.72 0.52 0.40 0.32 0.26","1 -3 5 -7 9 -11 13",8.0);
say(caption,"Only the moving tip writes history; the rest remain a readable mechanism.",0.45);
}
}
wait(0.45);
step("takeaway") {
cue(chime);
pulse(tip,0.7);
say(caption,"The same foundation serves epicycles, linkages, robot arms, and live derived plots.",0.45);
}
wait(1.2);
fourier-series-live-wave
Twelve visible orbit rings form one epicycle chain while historyplot3 turns every
endpoint’s exact y-history into a coloured partial sum across two complete periods—one
continuous motion, one source of truth.
// A focused Fourier story: every orbit and every partial sum is generated from
// the same live collection. The circles move; their histories become the wave.
title("A Wave Drawn by Rotating Circles");
canvas("9:16");
template("blank");
watermark(mark,(w*0.15,h*0.045),"Made With Manic");
text(kicker,(cx,h*0.075),"FOURIER SERIES · LIVE DERIVED HISTORY");
text(headline,(cx,h*0.115),"Can rotating circles draw a wave?");
text(caption,(cx,h*0.18),"Every coloured trace is the real history of one moving endpoint.");
text(cta,(cx,h*0.925),"BUILD YOUR OWN VISUAL STORY → 8gwifi.org/manic");
size(kicker,18); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,31); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(caption,20); color(caption,dim); wrap(caption,w*0.84); hidden(caption);
size(cta,21); color(cta,cyan); bold(cta); hidden(cta);
camera3((-29,-25,43),(1,-2,0),42,orthographic);
collection3(harmonics,(-3,5,0),12,(0,0,0),11,0.065);
line3(sweep,(-3,5,0),(10,5,0)); hidden(sweep);
links3(arms,harmonics,chain);
child3(tip,harmonics,11,0.13);
arrow3(chainX,(-11,5,0),(7,5,0));
arrow3(chainY,(-3,-2,0),(-3,10.5,0));
arrow3(plotX,(-7,-10.5,0),(12,-10.5,0));
arrow3(plotY,(-7,-10.5,0),(-7,-4.2,0));
point3(shotA,(-7.5,-11,0),0.01);
point3(shotB,(8.5,10.5,0),0.01);
tag(shotA,shot); tag(shotB,shot);
hidden(shotA); hidden(shotB);
tag(harmonics,construction); tag(arms,construction); tag(tip,construction);
tag(chainX,construction); tag(chainY,construction);
tag(plotX,construction); tag(plotY,construction);
for i in 0..12 {
ring3(orbit{i},harmonics,i,72);
historyplot3(partial{i},harmonics,i,y,(-7,-10.5,0),(19,6));
thick(orbit{i},0.030);
thick(partial{i},0.020);
tag(orbit{i},construction);
tag(partial{i},construction);
}
equation(formula,(0,0),`f(t)=\sum_{n=1}^{12}\frac{2(-1)^{n+1}}{\pi n}\sin(nt)`,25);
label3(formula,(-4,-2.7,0),0.82);
tag(formula,construction);
color(harmonics,fg); color(arms,fg); color(tip,fg);
color(chainX,fg); color(chainY,fg); color(plotX,fg); color(plotY,fg);
color(formula,fg);
thick(chainX,0.016); thick(chainY,0.016);
thick(plotX,0.016); thick(plotY,0.016);
color(orbit0,lime); color(partial0,lime);
color(orbit1,gold); color(partial1,gold);
color(orbit2,orange); color(partial2,orange);
color(orbit3,coral); color(partial3,coral);
color(orbit4,red); color(partial4,red);
color(orbit5,magenta); color(partial5,magenta);
color(orbit6,violet); color(partial6,violet);
color(orbit7,indigo); color(partial7,indigo);
color(orbit8,blue); color(partial8,blue);
color(orbit9,cyan); color(partial9,cyan);
color(orbit10,mint); color(partial10,mint);
color(orbit11,fg); color(partial11,fg);
hidden(construction);
step("ask the visual question") {
show(kicker,0.30);
show(headline,0.42);
show(caption,0.38);
}
wait(0.35);
step("reveal one connected machine") {
par {
show(construction,0.85);
view3(shot,"top",1.35,smooth,1.08);
say(caption,"Each circle starts where the previous circle ends.",0.38);
}
}
wait(0.30);
step("let motion become data") {
par {
// Doubling every angular rate makes the base harmonic complete exactly
// two revolutions while preserving all relative frequencies.
chain3(harmonics,"2.0372 -1.0186 0.6791 -0.5093 0.4074 -0.3395 0.2910 -0.2546 0.2264 -0.2037 0.1852 -0.1698","2 4 6 8 10 12 14 16 18 20 22 24",16.0);
travel3(harmonics,sweep,16.0,linear);
seq {
say(caption,"Cycle one: the broad motion appears, then smaller circles restore its detail.",0.40);
wait(6.10);
say(caption,"Cycle two: the same relationships repeat continuously—without a reset or jump.",0.40);
wait(5.20);
say(caption,"The endpoint histories now contain two complete periods of the signal.",0.40);
}
}
}
wait(0.35);
step("create with Manic") {
par {
pulse(tip,0.75);
pulse(partial11,0.75);
show(cta,0.45);
say(caption,"Describe the relationship. Manic keeps the circles, histories, motion, and camera together.",0.42);
}
}
wait(1.45);
definedmotion-fourier-gallery
Five depth-stacked Fourier families rotate at once while twenty coloured ring3
orbits drive twenty world-space partial histories per family. Focus and overview cameras
observe the same compiled motion before a creator CTA closes the Short.
// Visual acceptance story for DefinedMotion resources/animation2.gif.
// Twenty visible orbit rings create each relation. Every coloured partial-sum
// history is derived from the exact endpoint that draws it.
title("Fourier Series — Rotations Become Waves");
canvas("9:16");
template("mono");
watermark(mark,(w*0.15,h*0.045),"Made With Manic");
text(chapter,(cx,h*0.075),"FOURIER SERIES · FIVE RELATIONS");
text(caption,(cx,h*0.85),"Twenty rotating circles. One continuously derived signal.");
text(cta,(cx,h*0.925),"BUILD VISUAL EXPLANATIONS → 8gwifi.org/manic");
size(chapter,18); color(chapter,dim); bold(chapter); hidden(chapter);
size(caption,20); color(caption,dim); hidden(caption);
size(cta,21); color(cta,cyan); bold(cta); hidden(cta);
camera3((-31,-25,44),(1,-2,0),42,orthographic);
def relation(k,zv) {
// The chain, its two local axes, and the waveform axes all share one layer.
collection3(series{k},(-3,5,zv),20,(0,0,0),11,0.055);
line3(sweep{k},(-3,5,zv),(10,5,zv));
hidden(sweep{k});
links3(arms{k},series{k},chain);
child3(tip{k},series{k},19,0.12);
arrow3(chainX{k},(-11,5,zv),(7,5,zv));
arrow3(chainY{k},(-3,-3,zv),(-3,13,zv));
arrow3(plotX{k},(-7,-12,zv),(12,-12,zv));
arrow3(plotY{k},(-7,-12,zv),(-7,-3,zv));
// A cinematic crop: the axes may continue beyond the frame, as in the
// reference, while the active rings and the growing history stay dominant.
point3(shot{k}A,(-7.5,-10,zv),0.01);
point3(shot{k}B,(8.5,10,zv),0.01);
tag(shot{k}A,shot{k}); tag(shot{k}B,shot{k});
hidden(shot{k}A); hidden(shot{k}B);
tag(series{k},layer{k}); tag(arms{k},layer{k}); tag(tip{k},layer{k});
tag(chainX{k},layer{k}); tag(chainY{k},layer{k});
tag(plotX{k},layer{k}); tag(plotY{k},layer{k});
tag(layer{k},stack);
for i in 0..20 {
ring3(orbit{k}_{i},series{k},i,72);
historyplot3(wave{k}_{i},series{k},i,y,(-7,-12,zv),(19,8.5));
thick(orbit{k}_{i},0.022);
thick(wave{k}_{i},0.014);
tag(orbit{k}_{i},layer{k}); tag(wave{k}_{i},layer{k});
}
color(series{k},fg); color(arms{k},fg); color(tip{k},fg);
color(chainX{k},fg); color(chainY{k},fg);
color(plotX{k},fg); color(plotY{k},fg);
thick(chainX{k},0.016); thick(chainY{k},0.016);
thick(plotX{k},0.016); thick(plotY{k},0.016);
color(orbit{k}_0,lime); color(wave{k}_0,lime);
color(orbit{k}_1,gold); color(wave{k}_1,gold);
color(orbit{k}_2,gold); color(wave{k}_2,gold);
color(orbit{k}_3,magenta); color(wave{k}_3,magenta);
color(orbit{k}_4,coral); color(wave{k}_4,coral);
color(orbit{k}_5,red); color(wave{k}_5,red);
color(orbit{k}_6,orange); color(wave{k}_6,orange);
color(orbit{k}_7,magenta); color(wave{k}_7,magenta);
color(orbit{k}_8,violet); color(wave{k}_8,violet);
color(orbit{k}_9,indigo); color(wave{k}_9,indigo);
color(orbit{k}_10,cyan); color(wave{k}_10,cyan);
color(orbit{k}_11,blue); color(wave{k}_11,blue);
color(orbit{k}_12,mint); color(wave{k}_12,mint);
color(orbit{k}_13,lime); color(wave{k}_13,lime);
color(orbit{k}_14,gold); color(wave{k}_14,gold);
color(orbit{k}_15,cyan); color(wave{k}_15,cyan);
color(orbit{k}_16,lime); color(wave{k}_16,lime);
color(orbit{k}_17,magenta); color(wave{k}_17,magenta);
color(orbit{k}_18,coral); color(wave{k}_18,coral);
color(orbit{k}_19,cyan); color(wave{k}_19,cyan);
}
relation(0,-20);
relation(1,-10);
relation(2,0);
relation(3,10);
relation(4,20);
point3(overviewA,(-11,-12,-20),0.01);
point3(overviewB,(12,13,20),0.01);
tag(overviewA,overviewShot); tag(overviewB,overviewShot);
hidden(overviewA); hidden(overviewB);
equation(formula0,(0,0),`\sum_{n=1}^{N}\frac{4}{\pi(2n-1)}\sin((2n-1)t)`,26);
equation(formula1,(0,0),`\sum_{n=1}^{N}\frac{2(-1)^{n+1}}{\pi n}\sin(nt)`,26);
equation(formula2,(0,0),`\sum_{n=1}^{N}\frac{2w_n}{\pi n}\sin(nt)`,26);
equation(formula3,(0,0),`\sum_{n=1}^{N}\frac{2}{\pi n}\sin(n^2t)`,26);
equation(formula4,(0,0),`\sum_{n=1}^{N}\frac{2r_n}{\pi n}\sin(nt)`,26);
label3(formula0,(-8,-2,-20),0.82);
label3(formula1,(-8,-2,-10),0.82);
label3(formula2,(-8,-2,0),0.82);
label3(formula3,(-8,-2,10),0.82);
label3(formula4,(-8,-2,20),0.82);
for i in 0..5 {
tag(formula{i},layer{i}); tag(formula{i},stack); color(formula{i},fg);
}
hidden(stack);
step("enter the Fourier world") {
show(chapter,0.30);
show(caption,0.35);
show(stack,0.85);
}
wait(0.35);
step("twenty circles write five signals") {
par {
chain3(series0,"4.0744 1.3581 0.8149 0.5821 0.4527 0.3704 0.3134 0.2716 0.2397 0.2144 0.1940 0.1771 0.1630 0.1509 0.1405 0.1314 0.1235 0.1164 0.1101 0.1045","1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39",17.0);
chain3(series1,"2.0372 -1.0186 0.6791 -0.5093 0.4074 -0.3395 0.2910 -0.2546 0.2264 -0.2037 0.1852 -0.1698 0.1567 -0.1455 0.1358 -0.1273 0.1198 -0.1132 0.1072 -0.1019","1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20",17.0);
chain3(series2,"0 0.0276 0.0716 0.1154 0.1537 0.1838 0.2040 0.2136 0.2127 0.2023 0.1839 0.1595 0.1314 0.1020 0.0735 0.0480 0.0271 0.0119 0.0029 0","1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20",17.0);
chain3(series3,"2.0372 1.0186 0.6791 0.5093 0.4074 0.3395 0.2910 0.2546 0.2264 0.2037 0.1852 0.1698 0.1567 0.1455 0.1358 0.1273 0.1198 0.1132 0.1072 0.1019","1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 400",17.0);
chain3(series4,"1.3334 0.0973 0.0648 0.3333 0.4074 0.2222 0.0278 0.0243 0.1482 0.2037 0.1212 0.0162 0.0150 0.0952 0.1358 0.0833 0.0114 0.0108 0.0702 0.1019","1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20",17.0);
travel3(series0,sweep0,17.0,linear);
travel3(series1,sweep1,17.0,linear);
travel3(series2,sweep2,17.0,linear);
travel3(series3,sweep3,17.0,linear);
travel3(series4,sweep4,17.0,linear);
seq {
wait(0.45);
par {
fade(layer1,0.80); fade(layer2,0.80); fade(layer3,0.80); fade(layer4,0.80);
view3(shot0,"isometric",1.45,smooth,1.08);
say(caption,"Square wave — odd harmonics sharpen every corner.",0.35);
}
wait(1.65);
par {
fade(layer0,0.65); show(layer1,0.65);
view3(shot1,"isometric",1.35,smooth,1.08);
say(caption,"Sawtooth — every harmonic joins with alternating direction.",0.35);
}
wait(1.65);
par {
fade(layer1,0.65); show(layer2,0.65);
view3(shot2,"isometric",1.35,smooth,1.08);
say(caption,"Hann window — the outer harmonics taper away.",0.35);
}
wait(1.65);
par {
fade(layer2,0.65); show(layer3,0.65);
view3(shot3,"isometric",1.35,smooth,1.08);
say(caption,"Quadratic chirp — frequency accelerates as n squared.",0.35);
}
wait(1.65);
par {
fade(layer3,0.65); show(layer4,0.65);
view3(shot4,"isometric",1.35,smooth,1.08);
say(caption,"Envelope ripple — selected harmonics breathe in and out.",0.35);
}
wait(1.55);
par {
show(layer0,0.55); show(layer1,0.55); show(layer2,0.55); show(layer3,0.55);
view3(overviewShot,"isometric",1.5,smooth,1.30);
say(caption,"One relationship engine. Five very different signals.",0.35);
}
}
}
}
wait(0.35);
step("create with Manic") {
show(cta,0.45);
pulse(stack,0.75);
say(caption,"Describe the relationship; Manic keeps every ring, endpoint, history, and camera together.",0.40);
}
wait(1.4);
definedmotion-fourier-gallery-v2
The five-family gallery re-lit on the black full-colour screen: twenty coloured ring3
orbits per family rotate and draw twenty world-space histories, then a long final overview
holds on all five vivid signals at once—every strand readable against black.
// v2 of the Fourier gallery — on the BLACK template (the full-colour screen where
// semantic colours pop), short 9:16 canvas. Same twenty-ring engine and five
// relations, but the run is extended and the final "all five signals" overview
// HOLDS longer so the viewer gets a full, unhurried view of the complete colour
// composition — every strand vivid against black.
title("Fourier Series — Rotations Become Waves");
canvas("9:16");
template("black");
watermark(mark,(w*0.15,h*0.045),"Made With Manic");
text(chapter,(cx,h*0.075),"FOURIER SERIES · FIVE RELATIONS");
text(caption,(cx,h*0.85),"Twenty rotating circles. One continuously derived signal.");
text(cta,(cx,h*0.925),"BUILD VISUAL EXPLANATIONS → 8gwifi.org/manic");
size(chapter,18); color(chapter,dim); bold(chapter); hidden(chapter);
size(caption,20); color(caption,dim); hidden(caption);
size(cta,21); color(cta,cyan); bold(cta); hidden(cta);
camera3((-31,-25,44),(1,-2,0),42,orthographic);
def relation(k,zv) {
// The chain, its two local axes, and the waveform axes all share one layer.
collection3(series{k},(-3,5,zv),20,(0,0,0),11,0.055);
line3(sweep{k},(-3,5,zv),(10,5,zv));
hidden(sweep{k});
links3(arms{k},series{k},chain);
child3(tip{k},series{k},19,0.12);
arrow3(chainX{k},(-11,5,zv),(7,5,zv));
arrow3(chainY{k},(-3,-3,zv),(-3,13,zv));
arrow3(plotX{k},(-7,-12,zv),(12,-12,zv));
arrow3(plotY{k},(-7,-12,zv),(-7,-3,zv));
// A cinematic crop: the axes may continue beyond the frame, as in the
// reference, while the active rings and the growing history stay dominant.
point3(shot{k}A,(-7.5,-10,zv),0.01);
point3(shot{k}B,(8.5,10,zv),0.01);
tag(shot{k}A,shot{k}); tag(shot{k}B,shot{k});
hidden(shot{k}A); hidden(shot{k}B);
tag(series{k},layer{k}); tag(arms{k},layer{k}); tag(tip{k},layer{k});
tag(chainX{k},layer{k}); tag(chainY{k},layer{k});
tag(plotX{k},layer{k}); tag(plotY{k},layer{k});
tag(layer{k},stack);
for i in 0..20 {
ring3(orbit{k}_{i},series{k},i,72);
historyplot3(wave{k}_{i},series{k},i,y,(-7,-12,zv),(19,8.5));
thick(orbit{k}_{i},0.022);
thick(wave{k}_{i},0.014);
tag(orbit{k}_{i},layer{k}); tag(wave{k}_{i},layer{k});
}
color(series{k},fg); color(arms{k},fg); color(tip{k},fg);
color(chainX{k},fg); color(chainY{k},fg);
color(plotX{k},fg); color(plotY{k},fg);
thick(chainX{k},0.016); thick(chainY{k},0.016);
thick(plotX{k},0.016); thick(plotY{k},0.016);
color(orbit{k}_0,lime); color(wave{k}_0,lime);
color(orbit{k}_1,gold); color(wave{k}_1,gold);
color(orbit{k}_2,gold); color(wave{k}_2,gold);
color(orbit{k}_3,magenta); color(wave{k}_3,magenta);
color(orbit{k}_4,coral); color(wave{k}_4,coral);
color(orbit{k}_5,red); color(wave{k}_5,red);
color(orbit{k}_6,orange); color(wave{k}_6,orange);
color(orbit{k}_7,magenta); color(wave{k}_7,magenta);
color(orbit{k}_8,violet); color(wave{k}_8,violet);
color(orbit{k}_9,indigo); color(wave{k}_9,indigo);
color(orbit{k}_10,cyan); color(wave{k}_10,cyan);
color(orbit{k}_11,blue); color(wave{k}_11,blue);
color(orbit{k}_12,mint); color(wave{k}_12,mint);
color(orbit{k}_13,lime); color(wave{k}_13,lime);
color(orbit{k}_14,gold); color(wave{k}_14,gold);
color(orbit{k}_15,cyan); color(wave{k}_15,cyan);
color(orbit{k}_16,lime); color(wave{k}_16,lime);
color(orbit{k}_17,magenta); color(wave{k}_17,magenta);
color(orbit{k}_18,coral); color(wave{k}_18,coral);
color(orbit{k}_19,cyan); color(wave{k}_19,cyan);
}
relation(0,-20);
relation(1,-10);
relation(2,0);
relation(3,10);
relation(4,20);
point3(overviewA,(-11,-12,-20),0.01);
point3(overviewB,(12,13,20),0.01);
tag(overviewA,overviewShot); tag(overviewB,overviewShot);
hidden(overviewA); hidden(overviewB);
equation(formula0,(0,0),`\sum_{n=1}^{N}\frac{4}{\pi(2n-1)}\sin((2n-1)t)`,26);
equation(formula1,(0,0),`\sum_{n=1}^{N}\frac{2(-1)^{n+1}}{\pi n}\sin(nt)`,26);
equation(formula2,(0,0),`\sum_{n=1}^{N}\frac{2w_n}{\pi n}\sin(nt)`,26);
equation(formula3,(0,0),`\sum_{n=1}^{N}\frac{2}{\pi n}\sin(n^2t)`,26);
equation(formula4,(0,0),`\sum_{n=1}^{N}\frac{2r_n}{\pi n}\sin(nt)`,26);
label3(formula0,(-8,-2,-20),0.82);
label3(formula1,(-8,-2,-10),0.82);
label3(formula2,(-8,-2,0),0.82);
label3(formula3,(-8,-2,10),0.82);
label3(formula4,(-8,-2,20),0.82);
for i in 0..5 {
tag(formula{i},layer{i}); tag(formula{i},stack); color(formula{i},fg);
}
hidden(stack);
step("enter the Fourier world") {
show(chapter,0.30);
show(caption,0.35);
show(stack,0.85);
}
wait(0.35);
step("twenty circles write five signals") {
par {
chain3(series0,"4.0744 1.3581 0.8149 0.5821 0.4527 0.3704 0.3134 0.2716 0.2397 0.2144 0.1940 0.1771 0.1630 0.1509 0.1405 0.1314 0.1235 0.1164 0.1101 0.1045","1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39",22.0);
chain3(series1,"2.0372 -1.0186 0.6791 -0.5093 0.4074 -0.3395 0.2910 -0.2546 0.2264 -0.2037 0.1852 -0.1698 0.1567 -0.1455 0.1358 -0.1273 0.1198 -0.1132 0.1072 -0.1019","1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20",22.0);
chain3(series2,"0 0.0276 0.0716 0.1154 0.1537 0.1838 0.2040 0.2136 0.2127 0.2023 0.1839 0.1595 0.1314 0.1020 0.0735 0.0480 0.0271 0.0119 0.0029 0","1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20",22.0);
chain3(series3,"2.0372 1.0186 0.6791 0.5093 0.4074 0.3395 0.2910 0.2546 0.2264 0.2037 0.1852 0.1698 0.1567 0.1455 0.1358 0.1273 0.1198 0.1132 0.1072 0.1019","1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 400",22.0);
chain3(series4,"1.3334 0.0973 0.0648 0.3333 0.4074 0.2222 0.0278 0.0243 0.1482 0.2037 0.1212 0.0162 0.0150 0.0952 0.1358 0.0833 0.0114 0.0108 0.0702 0.1019","1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20",22.0);
travel3(series0,sweep0,22.0,linear);
travel3(series1,sweep1,22.0,linear);
travel3(series2,sweep2,22.0,linear);
travel3(series3,sweep3,22.0,linear);
travel3(series4,sweep4,22.0,linear);
seq {
wait(0.45);
par {
fade(layer1,0.80); fade(layer2,0.80); fade(layer3,0.80); fade(layer4,0.80);
view3(shot0,"isometric",1.45,smooth,1.08);
say(caption,"Square wave — odd harmonics sharpen every corner.",0.35);
}
wait(1.65);
par {
fade(layer0,0.65); show(layer1,0.65);
view3(shot1,"isometric",1.35,smooth,1.08);
say(caption,"Sawtooth — every harmonic joins with alternating direction.",0.35);
}
wait(1.65);
par {
fade(layer1,0.65); show(layer2,0.65);
view3(shot2,"isometric",1.35,smooth,1.08);
say(caption,"Hann window — the outer harmonics taper away.",0.35);
}
wait(1.65);
par {
fade(layer2,0.65); show(layer3,0.65);
view3(shot3,"isometric",1.35,smooth,1.08);
say(caption,"Quadratic chirp — frequency accelerates as n squared.",0.35);
}
wait(1.65);
par {
fade(layer3,0.65); show(layer4,0.65);
view3(shot4,"isometric",1.35,smooth,1.08);
say(caption,"Envelope ripple — selected harmonics breathe in and out.",0.35);
}
wait(1.55);
par {
show(layer0,0.55); show(layer1,0.55); show(layer2,0.55); show(layer3,0.55);
view3(overviewShot,"isometric",1.5,smooth,1.30);
say(caption,"One relationship engine. Five very different signals.",0.35);
}
// Hold on the full colour composition — all five signals rotating and drawing
// together — so the viewer gets an unhurried, complete view.
wait(6.5);
}
}
}
wait(0.6);
step("create with Manic") {
show(cta,0.45);
pulse(stack,0.75);
say(caption,"Describe the relationship; Manic keeps every ring, endpoint, history, and camera together.",0.40);
}
wait(3.0);
fourier-square-wave-live
Thirteen orbit rings add odd harmonics 1 through 25. Their live partial histories show the plateaus flatten and the corners sharpen across two uninterrupted square-wave cycles before a creator CTA closes the Short.
// Odd harmonics build a square wave. Every partial sum is a live history of
// the corresponding endpoint, so the corners emerge from the geometry itself.
title("How Odd Harmonics Build a Square Wave");
canvas("9:16");
template("paper");
watermark(mark,(w*0.15,h*0.045),"Made With Manic");
text(kicker,(cx,h*0.075),"FOURIER · ODD HARMONICS");
text(headline,(cx,h*0.115),"Watch a square wave find its corners");
text(caption,(cx,h*0.18),"Start with n = 1. Then add 3, 5, 7 … all the way to 25.");
text(cta,(cx,h*0.925),"TURN IDEAS INTO ANIMATION → 8gwifi.org/manic");
size(kicker,18); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,31); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(caption,20); color(caption,dim); wrap(caption,w*0.84); hidden(caption);
size(cta,21); color(cta,cyan); bold(cta); hidden(cta);
camera3((-29,-25,43),(1,-2,0),42,orthographic);
collection3(series,(-3,5,0),13,(0,0,0),11,0.065);
line3(sweep,(-3,5,0),(10,5,0)); hidden(sweep);
links3(arms,series,chain);
child3(tip,series,12,0.13);
arrow3(chainX,(-11,5,0),(7,5,0));
arrow3(chainY,(-3,-2,0),(-3,10.5,0));
arrow3(plotX,(-7,-10.5,0),(12,-10.5,0));
arrow3(plotY,(-7,-10.5,0),(-7,-4.2,0));
point3(shotA,(-7.5,-11,0),0.01);
point3(shotB,(8.5,10.5,0),0.01);
tag(shotA,shot); tag(shotB,shot);
hidden(shotA); hidden(shotB);
tag(series,construction); tag(arms,construction); tag(tip,construction);
tag(chainX,construction); tag(chainY,construction);
tag(plotX,construction); tag(plotY,construction);
for i in 0..13 {
ring3(orbit{i},series,i,72);
historyplot3(partial{i},series,i,y,(-7,-10.5,0),(19,6));
thick(orbit{i},0.030);
thick(partial{i},0.020);
tag(orbit{i},construction);
tag(partial{i},construction);
}
equation(formula,(0,0),`S_{13}(t)=\sum_{k=0}^{12}\frac{4}{\pi(2k+1)}\sin((2k+1)t)`,24);
label3(formula,(-4,-2.7,0),0.80);
tag(formula,construction);
color(series,fg); color(arms,fg); color(tip,fg);
color(chainX,fg); color(chainY,fg); color(plotX,fg); color(plotY,fg);
color(formula,fg);
thick(chainX,0.016); thick(chainY,0.016);
thick(plotX,0.016); thick(plotY,0.016);
color(orbit0,lime); color(partial0,lime);
color(orbit1,gold); color(partial1,gold);
color(orbit2,orange); color(partial2,orange);
color(orbit3,coral); color(partial3,coral);
color(orbit4,red); color(partial4,red);
color(orbit5,magenta); color(partial5,magenta);
color(orbit6,violet); color(partial6,violet);
color(orbit7,indigo); color(partial7,indigo);
color(orbit8,blue); color(partial8,blue);
color(orbit9,cyan); color(partial9,cyan);
color(orbit10,mint); color(partial10,mint);
color(orbit11,lime); color(partial11,lime);
color(orbit12,fg); color(partial12,fg);
hidden(construction);
step("begin with one sine wave") {
show(kicker,0.30);
show(headline,0.42);
show(caption,0.38);
}
wait(0.35);
step("reveal the odd-harmonic chain") {
par {
show(construction,0.85);
view3(shot,"top",1.35,smooth,1.08);
say(caption,"Every smaller orbit contributes the next odd frequency.",0.38);
}
}
wait(0.30);
step("build the corners live") {
par {
// Two complete base cycles: all odd harmonic rates are doubled together.
chain3(series,"4.0744 1.3581 0.8149 0.5821 0.4527 0.3704 0.3134 0.2716 0.2397 0.2144 0.1940 0.1771 0.1630","2 6 10 14 18 22 26 30 34 38 42 46 50",16.0);
travel3(series,sweep,16.0,linear);
seq {
say(caption,"Cycle one: odd harmonics flatten the plateaus and steepen both sides.",0.40);
wait(6.10);
say(caption,"Cycle two: every orbit closes and the square-wave pattern returns continuously.",0.40);
wait(5.20);
say(caption,"Two complete periods make the repeating structure impossible to miss.",0.40);
}
}
}
wait(0.35);
step("create with Manic") {
par {
pulse(tip,0.75);
pulse(partial12,0.75);
show(cta,0.45);
say(caption,"One declarative chain becomes circles, partial sums, a live waveform, and a complete visual story.",0.42);
}
}
wait(1.45);
growing-fractal-tree3
One seeded split-turn-shrink rule grows 1,023 branches. tree3 keeps each generation
renderer-batched while progressive draw and a bounds-aware camera reveal the canopy.
title("One Rule Grows a Forest");
canvas("9:16");
template("blank");
watermark(mark,(w*0.16,h*0.055),"Made With Manic");
text(kicker,(cx,h*0.085),"FRACTAL TREE · BATCHED GENERATIONS");
text(headline,(cx,h*0.13),"How can one branch become 1,023?");
text(caption,(cx,h*0.84),"Repeat one rule: split, turn, shrink—and keep every generation readable.");
text(cta,(cx,h*0.91),"CREATE YOUR OWN → 8gwifi.org/manic");
size(kicker,20); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,34); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(caption,22); color(caption,dim); wrap(caption,w*0.82); hidden(caption);
size(cta,22); color(cta,cyan); bold(cta); hidden(cta);
camera3((9,-13,8),(0,0,3.2),43);
tree3(tree,(0,0,-3.2),2.1,27,0.72,10,42);
for i in 0..10 { untraced(tree.d{i}); hidden(tree.d{i}); }
hidden(tree.leaves);
step("the seed") {
show(kicker,0.3); show(headline,0.45); show(caption,0.4);
view3(tree,"isometric",0.7,smooth,1.42);
}
wait(0.35);
step("grow by generation") {
par {
stagger(0.16) {
for i in 0..10 {
par { show(tree.d{i},0.22); draw(tree.d{i},0.58,smooth); }
}
}
say(caption,"Each depth is one renderer batch, even when it contains hundreds of branches.",0.45);
view3(tree,"isometric",1.1,smooth,1.48);
}
}
wait(0.4);
step("find the canopy") {
par {
show(tree.leaves,0.65);
seq {
orbit3(42,24,21.0,2.1,smooth);
view3(tree,"isometric",0.9,smooth,1.42);
}
say(caption,"A semantic camera journey reveals the rule first, then the whole canopy.",0.45);
}
}
wait(0.5);
step("takeaway") {
par {
pulse(tree.leaves,0.7);
say(caption,"Deep procedural geometry stays simple to author, deterministic, and smooth to seek.",0.45);
show(cta,0.45);
}
}
wait(1.3);
hilbert-curve3-space-filling
One continuous 3D Hilbert path refines from 7 to 32,767 segments while keeping its identity, arc-length colour, cube bounds, and uninterrupted orbit camera. The generated curve visits every lattice cell exactly once at each order.
// Engine Test 7 — a 3D Hilbert curve refines from 7 to 32,767 segments.
// One generated path keeps its identity while become3, draw, and orbit3 tell
// the story. The curve is exact: each order visits every lattice cell once.
title("Hilbert Curve in 3D");
canvas("16:9");
template("black");
creator(me,"@anish2good name=Manic_Math tagline=Space_filling_curves_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=magenta secondary=cyan footer=compact cta=Animate_the_infinite safe=clean");
watermark(mark,(105,52),"Made With Manic");
endcard(me,"title=Make_Fractals_Move cta=Create_with_Manic");
text(kicker,(cx,46),"HILBERT CURVE IN 3D");
text(head,(cx,86),"How can one line fill a cube?");
text(caption,(cx,626),"Begin with one continuous route through all eight octants.");
text(orderLabel,(cx,668),"ORDER 1 · 7 SEGMENTS");
size(kicker,20); bold(kicker); color(kicker,magenta);
size(head,34); bold(head);
size(caption,21); color(caption,dim);
size(orderLabel,19); bold(orderLabel);
camera3((13,-15,11),(0,0,0),42);
// A quiet wireframe establishes the volume before the curve enters it.
line3(e0,(-3,-3,-3),(3,-3,-3)); line3(e1,(3,-3,-3),(3,3,-3));
line3(e2,(3,3,-3),(-3,3,-3)); line3(e3,(-3,3,-3),(-3,-3,-3));
line3(e4,(-3,-3,3),(3,-3,3)); line3(e5,(3,-3,3),(3,3,3));
line3(e6,(3,3,3),(-3,3,3)); line3(e7,(-3,3,3),(-3,-3,3));
line3(e8,(-3,-3,-3),(-3,-3,3)); line3(e9,(3,-3,-3),(3,-3,3));
line3(e10,(3,3,-3),(3,3,3)); line3(e11,(-3,3,-3),(-3,3,3));
tag(e0,bounds); tag(e1,bounds); tag(e2,bounds); tag(e3,bounds);
tag(e4,bounds); tag(e5,bounds); tag(e6,bounds); tag(e7,bounds);
tag(e8,bounds); tag(e9,bounds); tag(e10,bounds); tag(e11,bounds);
color(bounds,dim); opacity(bounds,0.34); thick(bounds,0.012);
hidden(bounds); untraced(bounds);
// The five exact refinements. Hidden curves are blueprints for become3.
hilbert3(curve,(0,0,0),5.65,1);
hilbert3(order2,(0,0,0),5.65,2);
hilbert3(order3,(0,0,0),5.65,3);
hilbert3(order4,(0,0,0),5.65,4);
hilbert3(order5,(0,0,0),5.65,5);
hidden(curve); untraced(curve);
hidden(order2); hidden(order3); hidden(order4); hidden(order5);
hidden(kicker); hidden(head); hidden(caption); hidden(orderLabel);
step("pose the question") {
par {
show(kicker,0.30);
show(head,0.45);
show(caption,0.40);
show(orderLabel,0.35);
show(bounds,0.35);
draw(bounds,1.25,smooth);
}
}
wait(0.45);
step("visit the eight octants") {
par {
show(curve,0.30);
draw(curve,1.65,smooth);
orbit3(54,25,16.2,1.65,smooth);
}
}
wait(0.45);
step("refine the route") {
par {
seq {
par {
become3(curve,order2,1.30,smooth);
say(orderLabel,"ORDER 2 · 63 SEGMENTS",0.35,smooth);
say(caption,"Replace every visit with a smaller, rotated copy—without breaking the line.",0.45,smooth);
}
wait(0.35);
par {
become3(curve,order3,1.55,smooth);
say(orderLabel,"ORDER 3 · 511 SEGMENTS",0.35,smooth);
say(caption,"The gaps shrink while continuity survives every refinement.",0.45,smooth);
}
wait(0.35);
par {
become3(curve,order4,1.90,smooth);
say(orderLabel,"ORDER 4 · 4,095 SEGMENTS",0.35,smooth);
say(caption,"One path now threads through thousands of cells in the cube.",0.45,smooth);
}
wait(0.35);
par {
become3(curve,order5,2.40,smooth);
say(orderLabel,"ORDER 5 · 32,767 SEGMENTS",0.35,smooth);
say(caption,"In the limit, the continuous curve reaches every point in the volume.",0.50,smooth);
}
}
orbit3(186,31,17.8,8.55,linear);
}
}
wait(0.80);
step("takeaway") {
par {
pulse(curve,0.80);
say(head,"A one-dimensional path can fill 3D space.",0.50,smooth);
say(caption,"Refinement adds detail—not disconnected pieces.",0.45,smooth);
}
}
wait(1.35);
step("cta") {
par {
fade(kicker,0.30); fade(head,0.30); fade(caption,0.30);
fade(orderLabel,0.30); fade(bounds,0.40); fade(curve,0.45);
show(me.endcard,0.60);
}
}
wait(2.00);
latex-disintegration
A quadratic factors into two solutions, then those symbols become persistent points at
2 and 3. Semantic rewrite preserves the algebra; disintegrate removes the notation
while the mathematical meaning remains visibly in place.
// A semantic LaTeX story: notation changes, meaning becomes position, and the
// final symbols may disappear without taking the mathematical result with them.
title("The Equation Disappears. The Roots Remain.");
canvas("9:16");
template("blank");
watermark(mark,(w*0.16,h*0.050),"Made With Manic");
text(kicker,(cx,h*0.085),"ALGEBRA · SYMBOLS → STRUCTURE");
text(headline,(cx,h*0.145),"What survives when an equation disappears?");
text(caption,(cx,h*0.77),"Two solutions are hiding inside one quadratic.");
text(cta,(cx,h*0.90),"MAKE MEANING MOVE → 8gwifi.org/manic");
size(kicker,19); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,33); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(caption,22); color(caption,dim); wrap(caption,w*0.82); hidden(caption);
size(cta,22); color(cta,cyan); bold(cta); hidden(cta);
text(symbolLabel,(w*0.18,h*0.29),"THE SYMBOLS");
text(meaningLabel,(w*0.18,h*0.54),"THE MEANING");
size(symbolLabel,18); color(symbolLabel,dim); bold(symbolLabel); hidden(symbolLabel);
size(meaningLabel,18); color(meaningLabel,dim); bold(meaningLabel); hidden(meaningLabel);
equation(work,(cx,h*0.39),`x^2-5x+6=0`,66);
hidden(work);
let axisY = h*0.63;
let axisLeft = w*0.16;
let axisRight = w*0.84;
let x0 = w*0.20;
let dx = w*0.15;
arrow(numberAxis,(axisLeft,axisY),(axisRight,axisY));
color(numberAxis,dim); stroke(numberAxis,3);
tag(numberAxis,numberLine);
for i in 0..5 {
line(tick{i},(x0+i*dx,axisY-13),(x0+i*dx,axisY+13));
color(tick{i},dim); stroke(tick{i},3);
tag(tick{i},numberLine);
}
text(n0,(x0,axisY+42),"0");
text(n1,(x0+dx,axisY+42),"1");
text(n2,(x0+2*dx,axisY+42),"2");
text(n3,(x0+3*dx,axisY+42),"3");
text(n4,(x0+4*dx,axisY+42),"4");
size(n0,18); size(n1,18); size(n2,18); size(n3,18); size(n4,18);
color(n0,dim); color(n1,dim); color(n2,dim); color(n3,dim); color(n4,dim);
tag(n0,numberLine); tag(n1,numberLine); tag(n2,numberLine);
tag(n3,numberLine); tag(n4,numberLine);
circle(root2,(x0+2*dx,axisY),15);
circle(root3,(x0+3*dx,axisY),15);
filled(root2); filled(root3);
color(root2,cyan); color(root3,orange);
glow(root2,0.72); glow(root3,0.72);
tag(root2,solutions); tag(root3,solutions);
equation(root2Label,(x0+2*dx,axisY-62),`x=2`,30);
equation(root3Label,(x0+3*dx,axisY-62),`x=3`,30);
color(root2Label,cyan); color(root3Label,orange);
tag(root2Label,solutions); tag(root3Label,solutions);
hidden(numberLine);
hidden(solutions);
step("find what is hiding") {
par {
show(kicker,0.30);
show(headline,0.45);
show(symbolLabel,0.35);
show(caption,0.40);
show(work,0.55);
}
}
wait(0.55);
step("expose the two conditions") {
par {
rewrite(work,`\textcolor{cyan}{(x-2)}\textcolor{orange}{(x-3)}=0`,1.15,smooth);
say(caption,"Factoring reveals two independent ways for the product to become zero.",0.45);
}
}
wait(0.55);
step("turn answers into positions") {
par {
rewrite(work,`\textcolor{cyan}{x=2}\qquad\text{or}\qquad\textcolor{orange}{x=3}`,1.05,smooth);
seq {
show(meaningLabel,0.30);
show(numberLine,0.55);
show(root2,0.35);
show(root2Label,0.30);
wait(0.18);
show(root3,0.35);
show(root3Label,0.30);
}
say(caption,"The answers are no longer only symbols—they now occupy exact locations.",0.45);
}
}
wait(0.70);
step("remove the notation, keep the result") {
par {
disintegrate(work,1.60);
seq {
wait(0.35);
pulse(root2,0.60);
pulse(root3,0.60);
}
say(caption,"The notation can turn to dust. The two solutions remain exactly where they belong.",0.45);
}
}
wait(0.75);
step("create explanations that preserve meaning") {
par {
pulse(solutions,0.75);
show(cta,0.45);
say(caption,"Great visual explanations do more than move symbols—they reveal what those symbols mean.",0.45);
}
}
wait(1.45);
story-time-varying-field3
A seed cloud enters a 3D vector field whose formula changes with normalized time. Deterministic advection, live arrows, one addressable particle, its trail, and a follow camera all sample the same seekable timeline.
title("The Current Changes While We Travel");
canvas("9:16");
template("blank");
watermark(mark, (w*0.16, h*0.055), "Made With Manic");
text(kicker, (cx, h*0.085), "ENGINE FOUNDATION · LIVE 3D FIELDS");
text(headline, (cx, h*0.13), "What if the current changes mid-flight?");
text(caption, (cx, h*0.83), "The arrows and particles sample the same field at the same story time.");
size(kicker,20); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,34); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(caption,22); color(caption,dim); wrap(caption,w*0.80); hidden(caption);
camera3((16,-20,14),(0,0,0),46);
vectorfield3(current,(0,0,0),(4.2,3.2,2.6),"-y + 0.7*sin(p*tau)","x + 0.35*cos(p*tau)","0.45*sin(z+p*tau)",6);
collection3(tracers,(0,0,0),90,(2.5,1.8,1.5),73,0.065);
child3(hero,tracers,0,0.14);
trail3(route,tracers,0,0.032);
color(current,dim);
color(tracers,cyan);
color(hero,gold);
color(route,gold);
hidden(current); hidden(tracers); hidden(hero); hidden(route);
step("question") {
show(kicker,0.35);
show(headline,0.45);
show(caption,0.40);
}
wait(0.35);
step("reveal-the-current") {
par {
show(current,0.70);
show(tracers,0.55);
show(hero,0.45);
show(route,0.45);
say(caption,"A bounded formula declares x, y, z and normalized time p.",0.40);
}
}
wait(0.30);
step("flow-and-follow") {
followshot3(hero,(0,0,0.15));
par {
advect3(tracers,current,7.0,1.15);
orbit3(55,26,29.0,7.0,smooth);
say(caption,"Deterministic RK4 moves every tracer while the camera follows one stable child.",0.45);
}
followshot3(none);
}
wait(0.45);
step("takeaway") {
cue(chime);
say(caption,"Field, motion, trail, and shot now tell one synchronized truth.",0.45);
}
wait(1.2);
story-addressable-asset-cues
A grouped console OBJ becomes an addressable technical assembly. Its base, screen, and key reveal as named parts with projected notation, a local procedural cue, and particle punctuation—no remote asset or callback.
title("An Asset Becomes a Story");
canvas("9:16");
template("mono");
watermark(mark, (w*0.16, h*0.055), "Made With Manic");
text(kicker, (cx, h*0.085), "ENGINE FOUNDATION · ASSETS, LABELS & CUES");
text(headline, (cx, h*0.15), "An imported object should still be explainable");
text(caption, (cx, h*0.82), "Groups become addressable parts—then motion, notation, sound, and effects compose normally.");
size(kicker,18); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,34); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(caption,21); color(caption,dim); wrap(caption,w*0.82); hidden(caption);
camera3((16,-22,15),(0,0,0),46);
assembly3(console,"asset:models/manic-console.obj",(0,0,0),1.35);
color(console.base,dim);
color(console.screen,cyan);
color(console.key,magenta);
text(screenNote,(0,0),"world-sized display");
text(keyNote,(0,0),"addressable input");
size(screenNote,22); color(screenNote,cyan); hidden(screenNote);
size(keyNote,22); color(keyNote,magenta); hidden(keyNote);
label3(screenNote,console.screen,0.82);
label3(keyNote,console.key,0.82);
hidden(console);
circle(effectBox,(cx,h*0.70),72);
particles(sparks,effectBox,34,4,19);
color(sparks,gold);
hidden(effectBox); hidden(sparks);
step("import") {
show(kicker,0.35);
show(headline,0.45);
show(caption,0.40);
show(console,0.75);
cue(whoosh);
}
wait(0.35);
step("address-the-parts") {
seq {
show(screenNote,0.35);
pulse(console.screen,0.65);
fade(screenNote,0.25);
show(keyNote,0.35);
pulse(console.key,0.65);
}
say(caption,"OBJ groups arrive as console.base, console.screen, and console.key.",0.42);
cue(tick);
}
wait(0.35);
step("compose-the-explanation") {
par {
turn3(console,(0,0,0),z,22,1.4,smooth);
orbit3(30,28,30,1.4,smooth);
say(caption,"Depth-scaled labels remain crisp while the grouped asset moves.",0.40);
}
}
wait(0.30);
step("finish-with-a-beat") {
show(sparks,0.15);
par {
burst(sparks,0.9);
pulse(console.key,0.65);
say(caption,"A reusable cue and the existing generic burst add the final editorial beat.",0.40);
}
cue(pop);
}
wait(1.2);
trapped-light-dimensions
A photon escapes one dimension at a time: first a 5-unit line, then the 5–12–13 diagonal of a plane, and finally the 13–84–85 diagonal through a volume. One persistent light beam makes the generalized Pythagorean idea visible.
// ============================================================================
// trapped-light-dimensions.manic — a continuous 1D → 2D → 3D journey
// ----------------------------------------------------------------------------
// One photon and one equation persist through the whole story. A fiber sweeps
// sideways into a glass floor; that same floor rises into a transparent room.
// The visual construction makes the distance formula grow one perpendicular
// square at a time instead of presenting three unrelated formulas.
// ============================================================================
title("The Trapped Light Beam — From 1D to 3D");
canvas("9:16");
template("neon");
watermark(manicMark, (238, 108), "Made With Manic");
creator(me, "@anish2good name=Manic_3D tagline=Build_dimensions_visually yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker, (540, 164), "A JOURNEY THROUGH DIMENSIONS");
size(kicker, 21); bold(kicker); color(kicker, cyan); hidden(kicker);
text(headline, (540, 228), "The trapped light beam");
size(headline, 43); bold(headline); color(headline, fg); hidden(headline);
text(dimension, (540, 300), "1D · A THREAD IN THE DARK");
size(dimension, 23); bold(dimension); color(dimension, gold); hidden(dimension);
text(narration, (540, 1328), "No up. No down. Only backwards and forwards.");
size(narration, 27); bold(narration); color(narration, fg); wrap(narration, 860); hidden(narration);
text(insight, (540, 1386), "One permitted direction");
size(insight, 20); color(insight, dim); hidden(insight);
equation(distance, (540, 1482), `L=\left|x_2-x_1\right|`, 39);
hidden(distance);
// ---------------------------------------------------------------------------
// THE SPATIAL CAST
// ---------------------------------------------------------------------------
camera3((9, -16, 8), (2.5, 0, 0), 38);
// 1D: a five-unit fiber and one persistent photon.
line3(fiber, (0,0,0), (5,0,0)); color(fiber, cyan); thick(fiber, 0.065); untraced(fiber);
point3(A, (0,0,0), 0.16); color(A, fg); hidden(A);
point3(B, (5,0,0), 0.16); color(B, fg); hidden(B);
point3(photon, (0,0,0), 0.22); color(photon, gold); hidden(photon);
finish3(photon, "material=glass shading=smooth depth=0.35 shadow=0.20");
text(aLabel, (0,0), "A · x = 0"); size(aLabel, 21); color(aLabel, fg); hidden(aLabel); pin3(aLabel, A);
text(bLabel, (0,0), "B · x = 5"); size(bLabel, 21); color(bLabel, fg); hidden(bLabel); pin3(bLabel, B);
point3(xMid, (2.5,0,0), 0.01); hidden(xMid);
text(xLabel, (0,0), "a = 5"); size(xLabel, 22); bold(xLabel); color(xLabel, cyan); hidden(xLabel); pin3(xLabel, xMid);
// 2D: pull a copy of the fiber along y. The shallow cuboid is the glass floor.
line3(sweepLine, (0,0,0), (5,0,0)); color(sweepLine, magenta); thick(sweepLine, 0.045); hidden(sweepLine);
line3(yEdge, (0,0,0), (0,12,0)); color(yEdge, magenta); thick(yEdge, 0.045); untraced(yEdge); hidden(yEdge);
point3(yMid, (0,6,0), 0.01); hidden(yMid);
text(yLabel, (0,0), "b = 12"); size(yLabel, 22); bold(yLabel); color(yLabel, magenta); hidden(yLabel); pin3(yLabel, yMid);
cube3(glassFloor, (2.5,6,0), (5,12,0.12)); color(glassFloor, blue); hidden(glassFloor);
finish3(glassFloor, "material=glass shading=flat mesh=0.42 depth=0.30 shadow=0.12");
line3(floorDiagonal, (0,0,0.08), (5,12,0.08)); color(floorDiagonal, gold); thick(floorDiagonal, 0.075); untraced(floorDiagonal); hidden(floorDiagonal);
point3(floorTarget, (5,12,0.08), 0.19); color(floorTarget, fg); hidden(floorTarget);
text(floorTargetLabel, (0,0), "(5, 12)"); size(floorTargetLabel, 20); color(floorTargetLabel, fg); hidden(floorTargetLabel); pin3(floorTargetLabel, floorTarget);
point3(floorMid, (2.5,6,0.08), 0.01); hidden(floorMid);
text(floorPathLabel, (0,0), "base = 13"); size(floorPathLabel, 21); bold(floorPathLabel); color(floorPathLabel, gold); hidden(floorPathLabel); pin3(floorPathLabel, floorMid);
// 3D: the flat floor becomes a transparent room. The 84-unit height is
// compressed to 18 visual units so the reused 13-unit floor diagonal remains
// readable; labels and mathematics retain the real coordinates. Four persistent
// roof edges rise as one tagged rig while the transparent volume resolves.
cube3(room, (2.5,6,9), (5,12,18)); color(room, blue); hidden(room);
finish3(room, "material=glass shading=flat mesh=0.50 depth=0.38 shadow=0.16");
line3(roofFront, (0,0,0), (5,0,0)); color(roofFront, cyan); thick(roofFront, 0.055); hidden(roofFront); tag(roofFront, roof);
line3(roofBack, (0,12,0), (5,12,0)); color(roofBack, cyan); thick(roofBack, 0.055); hidden(roofBack); tag(roofBack, roof);
line3(roofLeft, (0,0,0), (0,12,0)); color(roofLeft, cyan); thick(roofLeft, 0.055); hidden(roofLeft); tag(roofLeft, roof);
line3(roofRight, (5,0,0), (5,12,0)); color(roofRight, cyan); thick(roofRight, 0.055); hidden(roofRight); tag(roofRight, roof);
line3(zEdge, (5,12,0), (5,12,18)); color(zEdge, lime); thick(zEdge, 0.08); untraced(zEdge); hidden(zEdge);
point3(zMid, (5,12,9), 0.01); hidden(zMid);
text(zLabel, (0,0), "c = 84"); size(zLabel, 22); bold(zLabel); color(zLabel, lime); hidden(zLabel); pin3(zLabel, zMid);
line3(spaceDiagonal, (0,0,0), (5,12,18)); color(spaceDiagonal, gold); thick(spaceDiagonal, 0.10); untraced(spaceDiagonal); hidden(spaceDiagonal);
point3(spaceTarget, (5,12,18), 0.24); color(spaceTarget, fg); hidden(spaceTarget);
text(spaceTargetLabel, (0,0), "(5, 12, 84)"); size(spaceTargetLabel, 20); color(spaceTargetLabel, fg); hidden(spaceTargetLabel); pin3(spaceTargetLabel, spaceTarget);
// ---------------------------------------------------------------------------
// THE JOURNEY
// ---------------------------------------------------------------------------
step("one dimension") {
seq {
par {
show(kicker, 0.35);
show(headline, 0.55);
show(dimension, 0.40);
show(narration, 0.45);
show(insight, 0.45);
show(distance, 0.55);
view3(fiber, "front", 1.0, smooth, 1.65);
}
stagger(0.12) {
show(A, 0.30);
show(aLabel, 0.30);
draw(fiber, 1.10, smooth);
show(B, 0.30);
show(bLabel, 0.30);
show(xLabel, 0.30);
show(photon, 0.35);
}
wait(0.45);
}
}
step("the bound beam") {
seq {
say(narration, "Fire one photon from A to B. There is nowhere else to go.", 0.50, smooth);
par {
travel3(photon, fiber, 2.25, smooth);
rewrite(distance, `L=\left|5-0\right|=\textcolor{gold}{5}`, 1.10, smooth);
say(insight, "Distance is the length of the only available track", 0.45, smooth);
}
pulse(photon, 0.70);
wait(0.55);
}
}
step("unlock the second direction") {
seq {
par {
move3(photon, (0,0,0), 0.80, smooth);
fade(aLabel, 0.35);
fade(bLabel, 0.35);
say(dimension, "2D · UNFOLDING THE FLOOR", 0.45, smooth);
say(narration, "Pull the fiber sideways. One line sweeps out a reflective surface.", 0.55, smooth);
say(insight, "A new perpendicular direction appears: y", 0.45, smooth);
}
par {
show(yEdge, 0.15);
draw(yEdge, 1.10, smooth);
show(yLabel, 0.35);
view3(glassFloor, "isometric", 1.35, smooth, 1.55);
}
par {
show(sweepLine, 0.25);
move3(sweepLine, (0,12,0), 1.90, smooth);
show(glassFloor, 1.90, smooth);
}
to(glassFloor, opacity, 0.22, 0.40, smooth);
wait(0.45);
}
}
step("cut across the floor") {
seq {
par {
show(floorTarget, 0.30);
show(floorTargetLabel, 0.35);
show(floorPathLabel, 0.35);
say(narration, "The photon can turn now — but the straight diagonal is shortest.", 0.55, smooth);
say(insight, "The old distance and the new sweep meet at 90 degrees", 0.45, smooth);
}
par {
show(floorDiagonal, 0.15);
draw(floorDiagonal, 2.35, smooth);
travel3(photon, floorDiagonal, 2.35, smooth);
rewrite(distance, `L=\sqrt{\textcolor{cyan}{5^2}+\textcolor{magenta}{12^2}}=\sqrt{169}=\textcolor{gold}{13}`, 1.35, smooth);
}
pulse(photon, 0.70);
wait(0.65);
}
}
step("raise the roof") {
seq {
par {
move3(photon, (0,0,0), 0.90, smooth);
fade(xLabel, 0.35);
fade(yLabel, 0.35);
fade(floorTargetLabel, 0.35);
say(dimension, "3D · FILLING THE VOID", 0.45, smooth);
say(narration, "Lift the entire floor upward. Surface becomes volume.", 0.55, smooth);
say(insight, "Height compressed visually · coordinates and geometry stay exact", 0.45, smooth);
}
par {
to(room, opacity, 0.22, 2.45, smooth);
show(roof, 0.20);
shift3(roof, (0,0,18), 2.45, smooth);
view3(room, "isometric", 2.45, smooth, 1.62);
show(zEdge, 0.15);
draw(zEdge, 2.30, smooth);
show(zLabel, 0.45);
}
par { show(spaceTarget, 0.40); show(spaceTargetLabel, 0.40); }
wait(0.55);
}
}
step("cut through space") {
seq {
say(narration, "The photon cuts through open space: floor diagonal against height.", 0.55, smooth);
par {
show(spaceDiagonal, 0.15);
draw(spaceDiagonal, 2.80, smooth);
travel3(photon, spaceDiagonal, 2.80, smooth);
rewrite(distance, `L=\sqrt{\textcolor{gold}{13^2}+\textcolor{lime}{84^2}}=\sqrt{7225}=\textcolor{gold}{85}`, 1.50, smooth);
say(insight, "13² + 84² = 85²", 0.45, smooth);
}
pulse(photon, 0.80);
wait(0.70);
}
}
step("one recursive idea") {
seq {
par {
say(dimension, "THE PATTERN · ONE RIGHT ANGLE AT A TIME", 0.50, smooth);
say(narration, "Do not memorize three formulas. Build one distance recursively.", 0.55, smooth);
say(insight, "Each previous diagonal becomes the next dimension's base", 0.50, smooth);
rewrite(distance, `L=\sqrt{\textcolor{cyan}{a^2}+\textcolor{magenta}{b^2}+\textcolor{lime}{c^2}}`, 1.65, smooth);
view3(room, "fit", 2.20, smooth, 1.70);
}
par {
pulse(photon, 0.80);
flash(spaceDiagonal, gold);
roll3(3, 1.60, smooth);
}
wait(2.20);
}
}
dimensions-unfold
A point stretches into a line, the line sweeps sideways into a plane, and the plane lifts into a room. The geometry grows continuously instead of resetting between 1D, 2D, and 3D.
// ============================================================================
// dimensions-unfold.manic — how a point becomes a line, a plane, then a room
// ----------------------------------------------------------------------------
// The geometry never cuts to a replacement scene. One point grows two halves
// of a wire; that same wire sweeps across y while a grid resolves behind it;
// the completed sheet then lifts as one tagged roof while a transparent room
// appears below. The animation is the explanation.
// ============================================================================
title("How Space Learned to Grow — 1D to 3D");
canvas("9:16");
template("neon");
watermark(manicMark, (238, 108), "Made With Manic");
creator(me, "@anish2good name=Manic_3D tagline=Dimensions_in_motion yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker, (540, 164), "FROM A POINT TO A WORLD");
size(kicker, 21); bold(kicker); color(kicker, cyan); hidden(kicker);
text(headline, (540, 228), "How space learned to grow");
size(headline, 42); bold(headline); color(headline, fg); hidden(headline);
text(phase, (540, 300), "PROLOGUE · THE FIRST POINT");
size(phase, 23); bold(phase); color(phase, gold); hidden(phase);
text(narration, (540, 1330), "In the beginning, there was only one location.");
size(narration, 27); bold(narration); color(narration, fg); wrap(narration, 860); hidden(narration);
text(principle, (540, 1390), "No direction. No distance. Just here.");
size(principle, 20); color(principle, dim); hidden(principle);
text(progress, (540, 1492), "POINT");
size(progress, 27); bold(progress); color(progress, cyan); hidden(progress);
text(takeaway, (540, 1554), "DIMENSIONS ARE MOTIONS THAT LEAVE SPACE BEHIND");
size(takeaway, 17); bold(takeaway); color(takeaway, gold); hidden(takeaway);
// ---------------------------------------------------------------------------
// 0D → 1D: one point extends left and right.
// ---------------------------------------------------------------------------
camera3((0, -11, 4.5), (0, -4, 0), 39);
point3(seed, (0,-4,0), 0.12); color(seed, gold); hidden(seed);
finish3(seed, "material=glass shading=smooth depth=0.40 shadow=0.25");
line3(leftThread, (0,-4,0), (-0.02,-4,0)); color(leftThread, cyan); thick(leftThread, 0.065); hidden(leftThread); tag(leftThread, wire); tag(leftThread, sheetSkin);
line3(rightThread, (0,-4,0), (0.02,-4,0)); color(rightThread, cyan); thick(rightThread, 0.065); hidden(rightThread); tag(rightThread, wire); tag(rightThread, sheetSkin);
point3(leftEnd, (-4,-4,0), 0.14); color(leftEnd, fg); hidden(leftEnd);
point3(rightEnd, (4,-4,0), 0.14); color(rightEnd, fg); hidden(rightEnd);
point3(leftLabelAnchor, (-3.25,-4,0.58), 0.01); hidden(leftLabelAnchor);
point3(rightLabelAnchor, (3.25,-4,0.58), 0.01); hidden(rightLabelAnchor);
text(leftLabel, (0,0), "BACKWARD"); size(leftLabel, 19); bold(leftLabel); color(leftLabel, dim); hidden(leftLabel); pin3(leftLabel, leftLabelAnchor);
text(rightLabel, (0,0), "FORWARD"); size(rightLabel, 19); bold(rightLabel); color(rightLabel, dim); hidden(rightLabel); pin3(rightLabel, rightLabelAnchor);
arrow3(leftChoice, (0,-4,0.18), (-3.5,-4,0.18)); color(leftChoice, magenta); thick(leftChoice, 0.035); untraced(leftChoice); hidden(leftChoice);
arrow3(rightChoice, (0,-4,0.18), (3.5,-4,0.18)); color(rightChoice, magenta); thick(rightChoice, 0.035); untraced(rightChoice); hidden(rightChoice);
// ---------------------------------------------------------------------------
// 2D: the wire sweeps from y=-4 to y=4 and leaves a sheet/grid behind.
// Every visible grid member joins sheetSkin so the whole plane can later rise.
// ---------------------------------------------------------------------------
line3(baseEdge, (-4,-4,0), (4,-4,0)); color(baseEdge, cyan); thick(baseEdge, 0.050); hidden(baseEdge); tag(baseEdge, sheetSkin);
cube3(sheet, (0,0,0), (8,8,0.08)); color(sheet, blue); hidden(sheet); tag(sheet, sheetSkin);
finish3(sheet, "material=glass shading=flat mesh=0.18 depth=0.24 shadow=0.10");
for i in 1..8 {
let p = -4+i;
line3(row{i}, (-4,p,0.05), (4,p,0.05));
color(row{i}, magenta); thick(row{i}, 0.022); untraced(row{i}); hidden(row{i}); tag(row{i}, sheetGrid); tag(row{i}, sheetSkin);
line3(col{i}, (p,-4,0.05), (p,4,0.05));
color(col{i}, cyan); thick(col{i}, 0.022); untraced(col{i}); hidden(col{i}); tag(col{i}, sheetGrid); tag(col{i}, sheetSkin);
}
// ---------------------------------------------------------------------------
// 3D: the complete sheet becomes the roof. Four pillars and a restrained glass
// volume make the hollow space below legible without a replacement cut.
// ---------------------------------------------------------------------------
cube3(room, (0,0,4), (8,8,8)); color(room, blue); hidden(room);
finish3(room, "material=glass shading=flat mesh=0.42 depth=0.34 shadow=0.16");
line3(pillar0, (-4,-4,0), (-4,-4,8)); color(pillar0, lime); thick(pillar0, 0.060); untraced(pillar0); hidden(pillar0); tag(pillar0, pillars);
line3(pillar1, (4,-4,0), (4,-4,8)); color(pillar1, lime); thick(pillar1, 0.060); untraced(pillar1); hidden(pillar1); tag(pillar1, pillars);
line3(pillar2, (-4,4,0), (-4,4,8)); color(pillar2, lime); thick(pillar2, 0.060); untraced(pillar2); hidden(pillar2); tag(pillar2, pillars);
line3(pillar3, (4,4,0), (4,4,8)); color(pillar3, lime); thick(pillar3, 0.060); untraced(pillar3); hidden(pillar3); tag(pillar3, pillars);
point3(nearCorner, (-4,-4,0), 0.18); color(nearCorner, gold); hidden(nearCorner);
point3(farCorner, (4,4,8), 0.18); color(farCorner, gold); hidden(farCorner);
text(nearLabel, (0,0), "NEAR · BOTTOM · LEFT"); size(nearLabel, 18); color(nearLabel, fg); hidden(nearLabel); pin3(nearLabel, nearCorner);
text(farLabel, (0,0), "FAR · TOP · RIGHT"); size(farLabel, 18); color(farLabel, fg); hidden(farLabel); pin3(farLabel, farCorner);
// ---------------------------------------------------------------------------
// ONE CONTINUOUS TIMELINE
// ---------------------------------------------------------------------------
step("the first point") {
seq {
par {
show(kicker, 0.35);
show(headline, 0.55);
show(phase, 0.40);
show(narration, 0.45);
show(principle, 0.45);
show(progress, 0.40);
show(seed, 0.45);
}
pulse(seed, 0.80);
wait(0.65);
}
}
step("the point extends") {
seq {
par {
say(phase, "1D · THE FIRST THREAD", 0.45, smooth);
say(narration, "The point stretches itself out and creates the first path.", 0.55, smooth);
say(principle, "Length appears — but the world is still trapped on one track.", 0.50, smooth);
say(progress, "POINT → LENGTH", 0.45, smooth);
show(leftThread, 0.15);
show(rightThread, 0.15);
grow3(leftThread, (-4,-4,0), 2.25, smooth);
grow3(rightThread, (4,-4,0), 2.25, smooth);
view3(wire, "front", 1.45, smooth, 1.70);
}
par {
fade(seed, 0.35);
show(leftEnd, 0.30); show(rightEnd, 0.30);
show(leftLabel, 0.35); show(rightLabel, 0.35);
show(leftChoice, 0.15); show(rightChoice, 0.15);
draw(leftChoice, 0.90, smooth); draw(rightChoice, 0.90, smooth);
}
wait(0.60);
}
}
step("left or right") {
seq {
say(narration, "Every journey has only two choices: backward or forward.", 0.50, smooth);
par { pulse(leftThread, 0.65); pulse(rightThread, 0.65); }
wait(0.80);
}
}
step("the sideways sweep") {
seq {
par {
fade(leftChoice, 0.35); fade(rightChoice, 0.35);
fade(leftLabel, 0.35); fade(rightLabel, 0.35);
say(phase, "2D · THE UNFOLDING SHEET", 0.45, smooth);
say(narration, "The wire shimmers, grabs its whole length, and moves sideways.", 0.55, smooth);
say(principle, "Its motion leaves a flat floor behind: width is born.", 0.50, smooth);
say(progress, "POINT → LENGTH → WIDTH", 0.45, smooth);
}
par {
show(baseEdge, 0.18);
shift3(wire, (0,8,0), 2.80, smooth);
to(sheet, opacity, 0.14, 2.80, smooth);
view3(sheet, "isometric", 2.10, smooth, 1.52);
stagger(0.075) { for i in 1..8 { show(row{i}, 0.12); show(col{i}, 0.12); } }
stagger(0.075) { for i in 1..8 { draw(row{i}, 0.70, smooth); draw(col{i}, 0.70, smooth); } }
}
wait(0.65);
}
}
step("the flat world") {
seq {
say(narration, "The old wire is now one edge of an entire plane.", 0.50, smooth);
say(principle, "Forward, backward, left, and right — still no above.", 0.45, smooth);
pulse(sheetGrid, 0.75);
wait(0.80);
}
}
step("the rising room") {
seq {
par {
say(phase, "3D · THE RISING ROOM", 0.45, smooth);
say(narration, "The complete sheet catches a breath and lifts straight upward.", 0.55, smooth);
say(principle, "The moving surface fills the hollow space below it.", 0.50, smooth);
say(progress, "POINT → LENGTH → WIDTH → HEIGHT", 0.50, smooth);
}
par {
shift3(sheetSkin, (0,0,8), 2.85, smooth);
to(room, opacity, 0.16, 2.85, smooth);
show(pillars, 0.15);
draw(pillars, 2.70, smooth);
view3(room, "isometric", 2.85, smooth, 1.52);
}
wait(0.65);
}
}
step("the space within") {
seq {
par {
show(nearCorner, 0.30); show(farCorner, 0.30);
show(nearLabel, 0.35); show(farLabel, 0.35);
show(takeaway, 0.45);
say(phase, "THE SPACE WITHIN", 0.45, smooth);
say(narration, "Depth rushes in. Near and far finally have meaning.", 0.55, smooth);
say(principle, "A dimension is an old world moving in a new perpendicular direction.", 0.55, smooth);
view3(room, "fit", 1.80, smooth, 1.62);
}
par {
pulse(sheetSkin, 0.85);
pulse(pillars, 0.85);
roll3(3, 1.80, smooth);
}
wait(2.20);
}
}
textbook-length-area-volume
Why units become cm, cm², and cm³: one measured segment sweeps out a rectangle, then the rectangle rises into a cuboid. A textbook measurement story built from extrusion rather than three disconnected formulas.
// ============================================================================
// textbook-length-area-volume.manic — centimetres, square centimetres, cubes
// ============================================================================
title("Length, Area, Volume — Why the Units Change");
canvas("9:16");
template("blueprint");
watermark(manicMark, (220, 175), "Made With Manic");
creator(me, "@anish2good name=Manic_Math tagline=Textbooks_in_motion yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker, (540,164), "THE MEASUREMENT LADDER"); size(kicker,21); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline, (540,228), "Why units become squared, then cubed"); size(headline,36); bold(headline); hidden(headline);
text(phase, (540,300), "1D · MEASURE LENGTH"); size(phase,23); bold(phase); color(phase,gold); hidden(phase);
text(narration, (540,1328), "A ruler counts how many unit segments fit along one direction."); size(narration,26); bold(narration); wrap(narration,850); hidden(narration);
text(principle, (540,1388), "One direction · centimetres"); size(principle,20); color(principle,dim); hidden(principle);
equation(measure, (540,1480), `L=\textcolor{cyan}{6}\text{ cm}`, 40); hidden(measure);
text(takeaway, (540,1560), "THE UNIT CHANGES BECAUSE THE NUMBER OF DIRECTIONS CHANGES"); size(takeaway,18); bold(takeaway); color(takeaway,gold); hidden(takeaway);
// This is the settled `view3(length,"front",...,1.65)` composition: the
// opening line is already inside the Creator media region, so frame one moves
// the mathematics rather than jolting the camera into place.
camera3((0.26,-33.6,-4.14),(0.26,-2,-4.14),40);
line3(length, (-3,-2,0), (3,-2,0)); color(length,cyan); thick(length,0.07); untraced(length); tag(length,areaSkin);
point3(start, (-3,-2,0),0.15); color(start,fg); hidden(start);
point3(finish, (3,-2,0),0.15); color(finish,fg); hidden(finish);
point3(lengthMid, (0,-2,0),0.01); hidden(lengthMid);
text(lengthLabel,(0,0),"6 cm"); size(lengthLabel,22); bold(lengthLabel); color(lengthLabel,cyan); hidden(lengthLabel); pin3(lengthLabel,lengthMid);
// Six visible unit segments sit directly on the measured line.
for i in 0..6 {
line3(unit{i}, (-3+i,-2,0.10), (-2+i,-2,0.10));
color(unit{i},gold); thick(unit{i},0.035); untraced(unit{i}); hidden(unit{i}); tag(unit{i},units1d); tag(unit{i},areaSkin);
}
// The measured line sweeps four centimetres sideways to create 24 unit squares.
cube3(areaSheet, (0,0,0), (6,4,0.08)); color(areaSheet,blue); hidden(areaSheet); tag(areaSheet,areaSkin);
finish3(areaSheet,"material=glass shading=flat mesh=0.20 depth=0.25 shadow=0.10");
line3(areaBase,(-3,-2,0),(3,-2,0)); color(areaBase,cyan); thick(areaBase,0.05); hidden(areaBase); tag(areaBase,areaSkin);
for i in 1..4 {
let yy=-2+i;
line3(areaRow{i},(-3,yy,0.05),(3,yy,0.05)); color(areaRow{i},magenta); thick(areaRow{i},0.025); untraced(areaRow{i}); hidden(areaRow{i}); tag(areaRow{i},areaGrid); tag(areaRow{i},areaSkin);
}
for i in 0..7 {
let xx=-3+i;
line3(areaCol{i},(xx,-2,0.05),(xx,2,0.05)); color(areaCol{i},cyan); thick(areaCol{i},0.025); untraced(areaCol{i}); hidden(areaCol{i}); tag(areaCol{i},areaGrid); tag(areaCol{i},areaSkin);
}
point3(widthMid,(-3,0,0),0.01); hidden(widthMid);
text(widthLabel,(0,0),"4 cm"); size(widthLabel,22); bold(widthLabel); color(widthLabel,magenta); hidden(widthLabel); pin3(widthLabel,widthMid);
// The complete square-unit sheet rises three centimetres and leaves 72 cubes.
cube3(volumeBox,(0,0,1.5),(6,4,3)); color(volumeBox,blue); hidden(volumeBox);
finish3(volumeBox,"material=glass shading=flat mesh=0.48 depth=0.34 shadow=0.16");
line3(height0,(-3,-2,0),(-3,-2,3)); color(height0,lime); thick(height0,0.06); untraced(height0); hidden(height0); tag(height0,heightEdges);
line3(height1,(3,-2,0),(3,-2,3)); color(height1,lime); thick(height1,0.06); untraced(height1); hidden(height1); tag(height1,heightEdges);
line3(height2,(-3,2,0),(-3,2,3)); color(height2,lime); thick(height2,0.06); untraced(height2); hidden(height2); tag(height2,heightEdges);
line3(height3,(3,2,0),(3,2,3)); color(height3,lime); thick(height3,0.06); untraced(height3); hidden(height3); tag(height3,heightEdges);
point3(heightMid,(3,2,1.5),0.01); hidden(heightMid);
text(heightLabel,(0,0),"3 cm"); size(heightLabel,22); bold(heightLabel); color(heightLabel,lime); hidden(heightLabel); pin3(heightLabel,heightMid);
step("count one direction") {
seq {
par { show(kicker,0.35); show(headline,0.55); show(phase,0.40); show(narration,0.45); show(principle,0.40); show(measure,0.50); }
par { show(start,0.25); show(finish,0.25); draw(length,1.25,smooth); show(lengthLabel,0.35); }
par {
stagger(0.10) { for i in 0..6 { show(unit{i},0.10); } }
stagger(0.10) { for i in 0..6 { draw(unit{i},0.35,smooth); } }
}
wait(0.60);
}
}
step("sweep out area") {
seq {
par {
say(phase,"2D · COUNT UNIT SQUARES",0.45,smooth);
say(narration,"Move the six-centimetre length through four new centimetres.",0.55,smooth);
say(principle,"Two perpendicular directions · square centimetres",0.45,smooth);
rewrite(measure,`A=\textcolor{cyan}{6}\times\textcolor{magenta}{4}=\textcolor{gold}{24}\text{ cm}^2`,1.10,smooth);
fade(start,0.30); fade(finish,0.30);
view3(areaSheet,"isometric",1.25,smooth,1.48);
}
par {
shift3(units1d,(0,4,0),2.60,smooth);
shift3(length,(0,4,0),2.60,smooth);
show(areaBase,0.15);
to(areaSheet,opacity,0.15,2.60,smooth);
show(widthLabel,0.35);
stagger(0.07) { for i in 1..4 { show(areaRow{i},0.10); } for i in 0..7 { show(areaCol{i},0.10); } }
stagger(0.07) { for i in 1..4 { draw(areaRow{i},0.55,smooth); } for i in 0..7 { draw(areaCol{i},0.55,smooth); } }
}
wait(0.70);
}
}
step("see twenty four squares") {
seq {
say(narration,"Six columns by four rows: the floor contains 24 unit squares.",0.50,smooth);
par { pulse(areaGrid,0.75); pulse(areaSheet,0.75); }
wait(0.75);
}
}
step("stack into volume") {
seq {
par {
say(phase,"3D · STACK UNIT CUBES",0.45,smooth);
say(narration,"Lift the complete 24-square layer through three centimetres.",0.55,smooth);
say(principle,"Three perpendicular directions · cubic centimetres",0.45,smooth);
rewrite(measure,`V=\textcolor{gold}{24}\times\textcolor{lime}{3}=\textcolor{gold}{72}\text{ cm}^3`,1.10,smooth);
fade(lengthLabel,0.30); fade(widthLabel,0.30);
}
par {
shift3(areaSkin,(0,0,3),2.70,smooth);
to(volumeBox,opacity,0.17,2.70,smooth);
show(heightEdges,0.15); draw(heightEdges,2.55,smooth);
show(heightLabel,0.35);
view3(volumeBox,"isometric",2.70,smooth,1.50);
}
wait(0.70);
}
}
step("the measurement ladder") {
seq {
par {
show(takeaway,0.45);
say(phase,"LENGTH → AREA → VOLUME",0.50,smooth);
say(narration,"The number is not merely larger. The kind of unit has changed.",0.55,smooth);
say(principle,"cm measures a path · cm² tiles a surface · cm³ fills space",0.55,smooth);
view3(volumeBox,"fit",1.80,smooth,1.58);
}
par { pulse(volumeBox,0.85); pulse(heightEdges,0.85); roll3(3,1.60,smooth); }
wait(2.10);
}
}
textbook-coordinate-worlds
A point earns a longer address as dimensions unlock: x on a line, (x,y) on a plane, then (x,y,z) in space. Coordinates remain attached to the same idea while the world expands around it.
// ============================================================================
// textbook-coordinate-worlds.manic — number line → coordinate plane → space
// ============================================================================
title("A Point Gets an Address — From 1D to 3D");
canvas("9:16");
template("blueprint");
watermark(manicMark, (220, 175), "Made With Manic");
creator(me, "@anish2good name=Manic_Math tagline=Textbooks_in_motion yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker,(540,164),"COORDINATE GEOMETRY"); size(kicker,21); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline,(540,228),"A point gains one coordinate at a time"); size(headline,32); bold(headline); hidden(headline);
text(phase,(540,300),"1D · THE NUMBER LINE"); size(phase,23); bold(phase); color(phase,gold); hidden(phase);
text(narration,(540,1328),"One direction needs one number: move three units along x."); size(narration,26); bold(narration); wrap(narration,850); hidden(narration);
text(principle,(540,1392),"Every new perpendicular direction adds one coordinate."); size(principle,20); color(principle,dim); hidden(principle);
equation(address,(540,1490),`P=\textcolor{cyan}{(3)}`,44); hidden(address);
text(takeaway,(540,1572),"THE POINT STAYS THE SAME · ITS ADDRESS BECOMES MORE PRECISE"); size(takeaway,18); bold(takeaway); color(takeaway,gold); hidden(takeaway);
camera3((0,0,14),(0,0,0),38);
// One persistent point begins at the origin, then gains y and z coordinates.
point3(P,(0,0,0),0.18); color(P,gold); hidden(P);
text(pLabel,(0,0),"P"); size(pLabel,34); bold(pLabel); color(pLabel,gold); hidden(pLabel); label3(pLabel,P,0.34);
point3(origin,(0,0,0),0.10); color(origin,fg); hidden(origin);
text(originLabel,(0,0),"O"); size(originLabel,34); color(originLabel,dim); hidden(originLabel); label3(originLabel,origin,0.36);
line3(xAxis,(-4,0,0),(4,0,0)); color(xAxis,cyan); thick(xAxis,0.045); untraced(xAxis); hidden(xAxis); tag(xAxis,axes);
line3(yAxis,(0,-3,0),(0,3,0)); color(yAxis,magenta); thick(yAxis,0.045); untraced(yAxis); hidden(yAxis); tag(yAxis,axes);
line3(zAxis,(0,0,0),(0,0,4)); color(zAxis,lime); thick(zAxis,0.045); untraced(zAxis); hidden(zAxis); tag(zAxis,axes);
point3(xEnd,(4,0,0),0.01); hidden(xEnd);
point3(yEnd,(0,3,0),0.01); hidden(yEnd);
point3(zEnd,(0,0,4),0.01); hidden(zEnd);
text(xLabel,(0,0),"x"); size(xLabel,34); bold(xLabel); color(xLabel,cyan); hidden(xLabel); label3(xLabel,xEnd,0.38);
text(yLabel,(0,0),"y"); size(yLabel,34); bold(yLabel); color(yLabel,magenta); hidden(yLabel); label3(yLabel,yEnd,0.38);
text(zLabel,(0,0),"z"); size(zLabel,34); bold(zLabel); color(zLabel,lime); hidden(zLabel); label3(zLabel,zEnd,0.38);
// Tick marks remain quiet so the address, rather than decoration, leads.
for i in -4..5 {
line3(xTick{i},(i,-0.10,0),(i,0.10,0)); color(xTick{i},cyan); thick(xTick{i},0.025); untraced(xTick{i}); hidden(xTick{i}); tag(xTick{i},xTicks);
}
for i in -3..4 {
line3(yTick{i},(-0.10,i,0),(0.10,i,0)); color(yTick{i},magenta); thick(yTick{i},0.025); untraced(yTick{i}); hidden(yTick{i}); tag(yTick{i},yTicks);
}
for i in 1..5 {
line3(zTick{i},(-0.10,0,i),(0.10,0,i)); color(zTick{i},lime); thick(zTick{i},0.025); untraced(zTick{i}); hidden(zTick{i}); tag(zTick{i},zTicks);
}
grid3(plane,(0,0,0),4,1); color(plane,blue); opacity(plane,0.22); hidden(plane); tag(plane,world2d);
cube3(space,(0,0,2),(8,6,4)); color(space,blue); hidden(space); tag(space,world3d);
finish3(space,"material=glass shading=flat mesh=0.18 depth=0.22 shadow=0.10");
// Guides expose how each coordinate is read from the axes.
line3(xGuide,(0,0,0),(3,0,0)); color(xGuide,cyan); thick(xGuide,0.065); untraced(xGuide); hidden(xGuide); tag(xGuide,guides);
line3(yGuide,(3,0,0),(3,2,0)); color(yGuide,magenta); thick(yGuide,0.065); untraced(yGuide); hidden(yGuide); tag(yGuide,guides);
line3(zGuide,(3,2,0),(3,2,3)); color(zGuide,lime); thick(zGuide,0.065); untraced(zGuide); hidden(zGuide); tag(zGuide,guides);
point3(xFoot,(3,0,0),0.11); color(xFoot,cyan); hidden(xFoot);
point3(xyFoot,(3,2,0),0.11); color(xyFoot,magenta); hidden(xyFoot);
project3(shadow,P,"xy"); color(shadow,magenta); hidden(shadow);
link3(drop,P,shadow,0.12); color(drop,lime); thick(drop,0.025); hidden(drop);
step("one number locates the point") {
seq {
par { show(kicker,0.35); show(headline,0.55); show(phase,0.40); show(narration,0.45); show(principle,0.40); show(address,0.50); view3(xAxis,"top",0.9,smooth,1.65); }
par { show(origin,0.20); show(originLabel,0.25); show(P,0.25); show(pLabel,0.25); draw(xAxis,1.10,smooth); show(xLabel,0.25); show(xTicks,0.35); }
par { shift3(P,(3,0,0),1.65,smooth); draw(xGuide,1.65,smooth); }
par { show(xFoot,0.25); pulse(P,0.65); pulse(address,0.65); }
wait(0.60);
}
}
step("the plane adds a second direction") {
seq {
par {
say(phase,"2D · THE COORDINATE PLANE",0.45,smooth);
say(narration,"Open a perpendicular y-direction, then move two units sideways.",0.55,smooth);
rewrite(address,`P=\textcolor{cyan}{(3},\textcolor{magenta}{2)}`,0.95,smooth);
show(plane,0.80); draw(yAxis,1.35,smooth); show(yLabel,0.30); show(yTicks,0.40);
view3(plane,"top",1.55,smooth,1.55);
}
par { shift3(P,(0,2,0),1.75,smooth); draw(yGuide,1.75,smooth); }
par { show(xyFoot,0.25); pulse(guides,0.80); pulse(address,0.80); }
wait(0.70);
}
}
step("read the ordered pair") {
seq {
say(narration,"First read x, then y. Order matters: (3,2) is not (2,3).",0.50,smooth);
par { pulse(xGuide,0.60); pulse(yGuide,0.60); }
wait(0.85);
}
}
step("space adds height") {
seq {
par {
say(phase,"3D · COORDINATE SPACE",0.45,smooth);
say(narration,"Raise a z-axis out of the plane and lift the point three units.",0.55,smooth);
rewrite(address,`P=\textcolor{cyan}{(3},\textcolor{magenta}{2},\textcolor{lime}{3)}`,1.05,smooth);
to(space,opacity,0.14,2.40,smooth);
draw(zAxis,1.55,smooth); show(zLabel,0.30); show(zTicks,0.40);
view3(space,"isometric",2.35,smooth,1.48);
}
par {
shift3(P,(0,0,3),2.15,smooth);
draw(zGuide,2.15,smooth);
show(shadow,0.30); show(drop,0.30);
}
par { pulse(P,0.70); pulse(address,0.70); }
wait(0.70);
}
}
step("one point three coordinates") {
seq {
par {
show(takeaway,0.45);
say(phase,"ONE ADDRESS · THREE DIRECTIONS",0.50,smooth);
say(narration,"x says across, y says sideways, and z says how high.",0.55,smooth);
say(principle,"1D: (x) · 2D: (x,y) · 3D: (x,y,z)",0.55,smooth);
view3(space,"fit",1.70,smooth,1.55);
}
par { pulse(axes,0.90); pulse(P,0.90); roll3(3,1.55,smooth); }
wait(1.80);
}
}
textbook-function-to-solid
A diameter becomes a semicircle and the semicircle revolves into a sphere. The story links a 1D domain, a 2D graph, and a 3D solid through one continuous generating motion.
// ============================================================================
// textbook-function-to-solid.manic — interval → graph → solid of revolution
// ============================================================================
title("The Revolving Semicircle — From 1D to 3D");
canvas("9:16");
template("neon");
watermark(manicMark,(220,175),"Made With Manic");
creator(me,"@anish2good name=Manic_Math tagline=Textbooks_in_motion yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker,(540,164),"SOLIDS OF REVOLUTION"); size(kicker,21); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline,(540,228),"A 1D domain grows into a 3D sphere"); size(headline,41); bold(headline); hidden(headline);
text(phase,(540,300),"1D · CHOOSE THE DOMAIN"); size(phase,23); bold(phase); color(phase,gold); hidden(phase);
text(narration,(540,1328),"Begin with every x-value from −2 to 2 on a number line."); size(narration,26); bold(narration); wrap(narration,850); hidden(narration);
text(principle,(540,1392),"The interval controls where the graph exists."); size(principle,20); color(principle,dim); hidden(principle);
equation(formula,(540,1488),`x\in\textcolor{cyan}{[-2,2]}`,44); hidden(formula);
text(takeaway,(540,1572),"DOMAIN → PROFILE → ROTATION → VOLUME"); size(takeaway,18); bold(takeaway); color(takeaway,gold); hidden(takeaway);
camera3((0,-14,1.8),(0,0,0.7),39);
// The x-axis and interval are the 1D seed for the whole construction.
line3(xAxis,(-2.6,0,0),(2.6,0,0)); color(xAxis,dim); thick(xAxis,0.035); untraced(xAxis); hidden(xAxis);
line3(domain,(-2,0,0),(2,0,0)); color(domain,cyan); thick(domain,0.09); untraced(domain); hidden(domain); tag(domain,profile);
point3(leftEnd,(-2,0,0),0.13); color(leftEnd,cyan); hidden(leftEnd);
point3(rightEnd,(2,0,0),0.13); color(rightEnd,cyan); hidden(rightEnd);
point3(origin,(0,0,0),0.09); color(origin,fg); hidden(origin);
point3(xEnd,(2.6,0,0),0.01); hidden(xEnd);
text(xLabel,(0,0),"x"); size(xLabel,23); bold(xLabel); color(xLabel,fg); hidden(xLabel); label3(xLabel,xEnd,0.30);
// The upper semicircle is a textbook profile: y = sqrt(4 - x^2).
curve3(semicircle,"-2+4*t","0","sqrt(4-(-2+4*t)^2)",(0,1)); color(semicircle,magenta); thick(semicircle,0.065); untraced(semicircle); hidden(semicircle); tag(semicircle,profile);
param3(region,
"-2+4*u",
"0",
"v*sqrt(4-(-2+4*u)^2)",
(0,1),(0,1),32);
color(region,magenta); hidden(region); tag(region,profile);
finish3(region,"material=glass shading=flat mesh=0.18 depth=0.18 shadow=0.08");
// Three sample heights explain how the graph rises from the domain.
line3(heightA,(-1,0,0),(-1,0,1.732)); color(heightA,gold); thick(heightA,0.035); untraced(heightA); hidden(heightA); tag(heightA,samples); tag(heightA,profile);
line3(heightB,(0,0,0),(0,0,2)); color(heightB,gold); thick(heightB,0.035); untraced(heightB); hidden(heightB); tag(heightB,samples); tag(heightB,profile);
line3(heightC,(1,0,0),(1,0,1.732)); color(heightC,gold); thick(heightC,0.035); untraced(heightC); hidden(heightC); tag(heightC,samples); tag(heightC,profile);
point3(apex,(0,0,2),0.10); color(apex,gold); hidden(apex); tag(apex,profile);
// Rotating the filled profile around x generates a radius-2 sphere.
param3(sphere,
"2*cos(u)",
"2*sin(u)*cos(v)",
"2*sin(u)*sin(v)",
(0,3.1416),(0,6.2832),38);
color(sphere,blue); hidden(sphere); tag(sphere,solid);
finish3(sphere,"material=glass shading=smooth mesh=0.24 depth=0.34 shadow=0.16");
curve3(equator,"0","2*cos(t)","2*sin(t)",(0,6.2832)); color(equator,cyan); thick(equator,0.035); untraced(equator); hidden(equator); tag(equator,solid);
curve3(meridian,"2*cos(t)","0","2*sin(t)",(0,6.2832)); color(meridian,magenta); thick(meridian,0.035); untraced(meridian); hidden(meridian); tag(meridian,solid);
step("mark the one dimensional domain") {
seq {
par { show(kicker,0.35); show(headline,0.55); show(phase,0.40); show(narration,0.45); show(principle,0.40); show(formula,0.50); view3(domain,"front",0.9,smooth,1.72); }
par { draw(xAxis,0.90,smooth); draw(domain,1.35,smooth); show(leftEnd,0.25); show(rightEnd,0.25); show(origin,0.25); show(xLabel,0.25); }
par { pulse(domain,0.75); pulse(formula,0.75); }
wait(0.65);
}
}
step("the function supplies height") {
seq {
par {
say(phase,"2D · DRAW THE PROFILE",0.45,smooth);
say(narration,"At each x, the function supplies a height above the axis.",0.55,smooth);
say(principle,"The interval becomes the base of an upper semicircle.",0.45,smooth);
rewrite(formula,`y=\textcolor{magenta}{\sqrt{4-x^2}},\quad -2\le x\le2`,1.05,smooth);
draw(semicircle,2.10,smooth);
stagger(0.22) { show(heightA,0.20); show(heightB,0.20); show(heightC,0.20); }
stagger(0.22) { draw(heightA,0.65,smooth); draw(heightB,0.65,smooth); draw(heightC,0.65,smooth); }
}
par { show(apex,0.25); pulse(samples,0.75); }
wait(0.70);
}
}
step("fill the generating region") {
seq {
par {
say(narration,"The curve and the x-axis enclose the region that will rotate.",0.50,smooth);
to(region,opacity,0.26,1.65,smooth);
view3(region,"front",1.40,smooth,1.62);
}
par { pulse(region,0.75); pulse(semicircle,0.75); }
wait(0.75);
}
}
step("rotate the profile into space") {
seq {
par {
say(phase,"3D · ROTATE ABOUT THE x-AXIS",0.45,smooth);
say(narration,"Sweep the entire semicircle through 360° around its diameter.",0.55,smooth);
say(principle,"Every point traces a circle; together those circles form a sphere.",0.50,smooth);
rewrite(formula,`x^2+y^2+z^2=\textcolor{gold}{4}`,1.05,smooth);
turn3(profile,(0,0,0),x,360,3.10,smooth);
to(sphere,opacity,0.34,3.10,smooth);
show(equator,0.45); draw(equator,2.15,smooth);
show(meridian,0.45); draw(meridian,2.15,smooth);
view3(solid,"isometric",2.60,smooth,1.55);
}
par { pulse(solid,0.85); roll3(4,1.50,smooth); }
wait(0.75);
}
}
step("read the solid of revolution") {
seq {
par {
show(takeaway,0.45);
say(phase,"THE SAME RADIUS · A NEW DIMENSION",0.50,smooth);
say(narration,"The 2D radius is still 2, but rotation has filled 3D volume.",0.55,smooth);
say(principle,"A semicircle rotated about its diameter generates a sphere.",0.50,smooth);
rewrite(formula,`V=\frac{4}{3}\pi(2)^3=\textcolor{gold}{\frac{32\pi}{3}}`,1.15,smooth);
view3(solid,"fit",1.65,smooth,1.58);
}
par { pulse(sphere,0.90); pulse(formula,0.90); roll3(3,1.50,smooth); }
wait(1.90);
}
}
textbook-statistical-dimensions
Data grows from a one-variable number line to a two-variable scatter plot and a three-variable point cloud. The axes and observations evolve together so statistical dimension reads as information, not decoration.
// ============================================================================
// textbook-statistical-dimensions.manic
// One variable → two variables → three variables, using the same 12 students.
// Stats kit: summary + correlation. 3D primitives reveal the third variable.
// ============================================================================
title("Statistical Dimensions — From a List to a Data Cloud");
canvas("9:16");
template("blueprint");
watermark(manicMark,(220,175),"Made With Manic");
creator(me,"@anish2good name=Manic_Stats tagline=Textbooks_in_motion yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker,(540,164),"MULTIVARIATE STATISTICS"); size(kicker,21); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline,(540,228),"The same students, in more dimensions"); size(headline,33); bold(headline); hidden(headline);
text(phase,(540,300),"1D · ONE VARIABLE"); size(phase,23); bold(phase); color(phase,gold); hidden(phase);
text(narration,(540,1328),"Record only height. Each student needs one number."); size(narration,26); bold(narration); wrap(narration,850); hidden(narration);
text(principle,(540,1392),"Centre and spread describe one-dimensional data."); size(principle,20); color(principle,dim); hidden(principle);
equation(address,(540,1488),`\text{student}=\textcolor{cyan}{(\text{height})}`,39); hidden(address);
text(takeaway,(540,1572),"MORE VARIABLES DO NOT ADD PEOPLE · THEY ADD STRUCTURE"); size(takeaway,18); bold(takeaway); color(takeaway,gold); hidden(takeaway);
let vx=518;
let vy=610;
// Twelve students, measured in centimetres.
summary(one,(vx,vy),
"150 154 158 160 164 167 170 173 176 180 184 188",760,cyan);
untraced(one.line); hidden(one.dots); hidden(one.band);
hidden(one.meanmark); hidden(one.meanlbl);
hidden(one.medianmark); hidden(one.medianlbl);
hidden(one.readout); hidden(one.min); hidden(one.max);
// The same students: height and arm span. Target points remain hidden because
// the 1D dots will `become` these blueprints, preserving one identity per row.
correlation(two,(vx,vy),10,
"150 151 154 153 158 160 160 159 164 165 167 168 170 169 173 175 176 174 180 181 184 185 188 187",cyan);
hidden(two.points); untraced(two.line); hidden(two.r);
// Front view: x = height, screen-up z = arm span. Depth y stores shoe size.
// The camera begins in the exact 2D projection; its later isometric move is
// what makes the third variable perceptible.
camera3((0.53,-64.4,-5.34),(0.53,0,-5.34),40);
cube3(dataRoom,(0,0,3.1),(7.2,5.6,6.2)); color(dataRoom,blue); hidden(dataRoom); tag(dataRoom,multivariate);
finish3(dataRoom,"material=glass shading=flat mesh=0.17 depth=0.22 shadow=0.10");
line3(heightAxis,(-3.6,-2.8,0),(3.6,-2.8,0)); color(heightAxis,cyan); thick(heightAxis,0.045); untraced(heightAxis); hidden(heightAxis); tag(heightAxis,multivariate); tag(heightAxis,axes3d);
line3(shoeAxis,(-3.6,-2.8,0),(-3.6,2.8,0)); color(shoeAxis,magenta); thick(shoeAxis,0.045); untraced(shoeAxis); hidden(shoeAxis); tag(shoeAxis,multivariate); tag(shoeAxis,axes3d);
line3(armAxis,(-3.6,-2.8,0),(-3.6,-2.8,6.2)); color(armAxis,lime); thick(armAxis,0.045); untraced(armAxis); hidden(armAxis); tag(armAxis,multivariate); tag(armAxis,axes3d);
point3(heightEnd,(3.6,-2.8,0),0.01); hidden(heightEnd);
point3(shoeEnd,(-3.6,2.8,0),0.01); hidden(shoeEnd);
point3(armEnd,(-3.6,-2.8,6.2),0.01); hidden(armEnd);
text(heightLabel,(0,0),"height"); size(heightLabel,25); bold(heightLabel); color(heightLabel,cyan); hidden(heightLabel); label3(heightLabel,heightEnd,0.40);
text(shoeLabel,(0,0),"shoe size"); size(shoeLabel,25); bold(shoeLabel); color(shoeLabel,magenta); hidden(shoeLabel); label3(shoeLabel,shoeEnd,0.40);
text(armLabel,(0,0),"arm span"); size(armLabel,25); bold(armLabel); color(armLabel,lime); hidden(armLabel); label3(armLabel,armEnd,0.40);
// Normalised triples: (height, shoe-size depth, arm-span height).
point3(p0,(-3.167,-2.4,0.000),0.13); color(p0,cyan); hidden(p0); tag(p0,cloud); tag(p0,multivariate);
point3(p1,(-2.500,-1.6,0.333),0.13); color(p1,cyan); hidden(p1); tag(p1,cloud); tag(p1,multivariate);
point3(p2,(-1.833,-1.6,1.500),0.13); color(p2,cyan); hidden(p2); tag(p2,cloud); tag(p2,multivariate);
point3(p3,(-1.500,-0.8,1.333),0.13); color(p3,cyan); hidden(p3); tag(p3,cloud); tag(p3,multivariate);
point3(p4,(-0.833,-0.8,2.333),0.13); color(p4,cyan); hidden(p4); tag(p4,cloud); tag(p4,multivariate);
point3(p5,(-0.333, 0.0,2.833),0.13); color(p5,cyan); hidden(p5); tag(p5,cloud); tag(p5,multivariate);
point3(p6,( 0.167, 0.0,3.000),0.13); color(p6,cyan); hidden(p6); tag(p6,cloud); tag(p6,multivariate);
point3(p7,( 0.667, 0.8,4.000),0.16); color(p7,gold); hidden(p7); tag(p7,cloud); tag(p7,multivariate);
point3(p8,( 1.167, 0.8,3.833),0.13); color(p8,cyan); hidden(p8); tag(p8,cloud); tag(p8,multivariate);
point3(p9,( 1.833, 1.6,5.000),0.13); color(p9,cyan); hidden(p9); tag(p9,cloud); tag(p9,multivariate);
point3(p10,(2.500, 1.6,5.667),0.13); color(p10,cyan); hidden(p10); tag(p10,cloud); tag(p10,multivariate);
point3(p11,(3.167, 2.4,6.000),0.13); color(p11,cyan); hidden(p11); tag(p11,cloud); tag(p11,multivariate);
project3(frontShadow,p7,"xz"); color(frontShadow,gold); hidden(frontShadow); tag(frontShadow,multivariate);
link3(depthLink,p7,frontShadow,0.14); color(depthLink,magenta); thick(depthLink,0.025); hidden(depthLink); tag(depthLink,multivariate);
text(depthNote,(0,0),"shoe size adds depth"); size(depthNote,24); bold(depthNote); color(depthNote,gold); hidden(depthNote); label3(depthNote,p7,0.42);
step("one variable centre and spread") {
seq {
par { show(kicker,0.35); show(headline,0.55); show(phase,0.40); show(narration,0.45); show(principle,0.40); show(address,0.55); }
draw(one.line,0.75,smooth);
par { show(one.min,0.25); show(one.max,0.25); stagger(0.09) { for i in 0..12 { show(one.dot{i},0.18); } } }
par { to(one.band,opacity,0.12,0.65,smooth); show(one.meanmark,0.35); show(one.meanlbl,0.35); show(one.medianmark,0.35); show(one.medianlbl,0.35); }
par { show(one.readout,0.35); pulse(one.dots,0.75); }
wait(0.70);
}
}
step("a second variable reveals relationship") {
seq {
par {
say(phase,"2D · TWO VARIABLES",0.45,smooth);
say(narration,"Add arm span. Every student now becomes an ordered pair.",0.55,smooth);
say(principle,"The slope of the cloud reveals how two measurements move together.",0.50,smooth);
rewrite(address,`\text{student}=\textcolor{cyan}{(\text{height},\text{arm span})}`,1.00,smooth);
fade(one.band,0.45); fade(one.meanmark,0.45); fade(one.meanlbl,0.45);
fade(one.medianmark,0.45); fade(one.medianlbl,0.45); fade(one.readout,0.45);
fade(one.line,0.45); fade(one.min,0.35); fade(one.max,0.35);
}
par {
for i in 0..12 { become(one.dot{i},two.p{i},1.75,smooth); }
}
par { draw(two.line,1.05,smooth); show(two.r,0.45); }
par { pulse(one.dots,0.75); pulse(two.r,0.75); }
wait(0.70);
}
}
step("the front projection still looks two dimensional") {
seq {
say(narration,"A front view can hide depth: the 3D records still resemble the scatterplot.",0.55,smooth);
par {
fade(one.dots,0.80);
fade(two.line,0.80);
fade(two.r,0.80);
stagger(0.08) { for i in 0..12 { show(p{i},0.18); } }
show(heightAxis,0.35); show(armAxis,0.35);
draw(heightAxis,1.10,smooth); draw(armAxis,1.10,smooth);
show(heightLabel,0.30); show(armLabel,0.30);
}
wait(0.75);
}
}
step("rotate to reveal the third variable") {
seq {
par {
say(phase,"3D · THREE VARIABLES",0.45,smooth);
say(narration,"Rotate the view. Shoe size reveals variation the flat plot could not show.",0.55,smooth);
say(principle,"A third coordinate turns a scatterplot into a multivariate cloud.",0.50,smooth);
rewrite(address,`\text{student}=\textcolor{cyan}{(\text{height},\text{arm span},\text{shoe})}`,1.10,smooth);
to(dataRoom,opacity,0.13,2.50,smooth);
show(shoeAxis,0.30); draw(shoeAxis,1.20,smooth); show(shoeLabel,0.30);
view3(multivariate,"isometric",2.65,smooth,1.52);
}
par { show(frontShadow,0.30); show(depthLink,0.35); show(depthNote,0.35); pulse(p7,0.75); }
wait(0.75);
}
}
step("dimensions describe not duplicate") {
seq {
par {
show(takeaway,0.45);
say(phase,"12 STUDENTS · 36 MEASUREMENTS",0.50,smooth);
say(narration,"The sample size stayed twelve; each observation simply gained context.",0.55,smooth);
say(principle,"1D describes · 2D relates · 3D separates hidden structure",0.55,smooth);
view3(multivariate,"fit",1.60,smooth,1.58);
}
par { pulse(cloud,0.90); pulse(address,0.90); roll3(3,1.50,smooth); }
wait(1.90);
}
}
textbook-geometry-dimension-reduction
The reverse journey: a sphere reveals a great-circle section, then that circle collapses to its diameter. A 3D→2D→1D geometry lesson that makes section and projection relationships explicit.
// ============================================================================
// textbook-geometry-dimension-reduction.manic
// Sphere → central cross-section → circle → diameter (3D → 2D → 1D).
// Geo kit: point, circle2, linecircle, segment, midpoint.
// ============================================================================
title("Dimension Reduction — From Sphere to Diameter");
canvas("9:16");
template("blueprint");
watermark(manicMark,(220,175),"Made With Manic");
creator(me,"@anish2good name=Manic_Geometry tagline=Textbooks_in_motion yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker,(540,164),"GEOMETRY BY DIMENSION"); size(kicker,21); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline,(540,228),"A sphere hides a circle — and a line"); size(headline,36); bold(headline); hidden(headline);
text(phase,(540,300),"3D · BEGIN WITH A SPHERE"); size(phase,23); bold(phase); color(phase,gold); hidden(phase);
text(narration,(540,1328),"A sphere extends in three independent directions."); size(narration,26); bold(narration); wrap(narration,850); hidden(narration);
text(principle,(540,1392),"Its radius is 3 units in every direction."); size(principle,20); color(principle,dim); hidden(principle);
equation(formula,(540,1488),`x^2+y^2+z^2=\textcolor{gold}{3^2}`,43); hidden(formula);
text(takeaway,(540,1572),"FIX A COORDINATE · LOSE A DIMENSION · KEEP THE MEASURE"); size(takeaway,18); bold(takeaway); color(takeaway,gold); hidden(takeaway);
// Settled isometric composition of the complete sphere inside the Creator
// media region. Starting here prevents an opening camera correction.
camera3((40.05,-39.35,23.41),(3.15,-2.45,-6.79),40);
sphere3(solid,(0,0,0),3); color(solid,blue); hidden(solid); tag(solid,spatial);
finish3(solid,"material=glass shading=smooth mesh=0.25 depth=0.36 shadow=0.16");
// A thin xz-plane begins outside the sphere and travels to y=0.
cube3(slicePlane,(0,-5,0),(7,0.08,7)); color(slicePlane,magenta); hidden(slicePlane); tag(slicePlane,spatial);
finish3(slicePlane,"material=glass shading=flat mesh=0.10 depth=0.18 shadow=0.06");
curve3(greatCircle,"3*cos(t)","0","3*sin(t)",(0,6.2832)); color(greatCircle,magenta); thick(greatCircle,0.065); untraced(greatCircle); hidden(greatCircle); tag(greatCircle,spatial);
// Screen-space geo construction aligned with the settled front projection.
let gx=518;
let gy=890;
let gr=145;
point(O,(gx,gy),"O"); hidden(O); hidden(O.label);
point(A,(gx+gr,gy),"R"); hidden(A); hidden(A.label);
circle2(section,O,A); color(section,magenta); stroke(section,4); untraced(section);
segment(radius2d,O,A); color(radius2d,gold); stroke(radius2d,4); untraced(radius2d);
// Two hidden points define a horizontal secant. The geo kit computes its exact
// circle intersections `ends0` and `ends1`, then derives their midpoint M.
point(lineLeft,(gx-gr-55,gy)); hidden(lineLeft);
point(lineRight,(gx+gr+55,gy)); hidden(lineRight);
linecircle(ends,lineLeft,lineRight,O,A);
hidden(ends0); hidden(ends1);
label(ends0,"A",(-18,-22)); hidden(ends0.label);
label(ends1,"B",(18,-22)); hidden(ends1.label);
segment(diameter,ends0,ends1); color(diameter,cyan); stroke(diameter,6); untraced(diameter);
midpoint(M,ends0,ends1); color(M,lime); hidden(M);
label(M,"M",(0,-24)); hidden(M.label);
step("the full three dimensional object") {
seq {
par { show(kicker,0.35); show(headline,0.55); show(phase,0.40); show(narration,0.45); show(principle,0.40); show(formula,0.55); }
show(solid,0.85);
par { pulse(solid,0.85); roll3(4,1.45,smooth); }
wait(0.70);
}
}
step("fix one coordinate with a plane") {
seq {
par {
say(phase,"3D → 2D · TAKE A CENTRAL SLICE",0.45,smooth);
say(narration,"Move a plane to the centre and fix y=0.",0.55,smooth);
say(principle,"Only points shared by the sphere and plane survive.",0.50,smooth);
rewrite(formula,`y=0\quad\Longrightarrow\quad x^2+z^2=\textcolor{gold}{3^2}`,1.05,smooth);
show(slicePlane,0.35);
}
par {
shift3(slicePlane,(0,5,0),2.10,smooth);
show(greatCircle,0.25); draw(greatCircle,2.10,smooth);
}
par { pulse(greatCircle,0.75); to(solid,opacity,0.04,0.65,smooth); fade(slicePlane,0.65); }
// A generous margin makes the spatial circle settle at the same on-screen
// radius as the following geo-kit circle, avoiding a dimensional size jump.
view3(greatCircle,"front",1.65,smooth,3.10);
wait(0.60);
}
}
step("the cross section becomes a geo circle") {
seq {
par {
say(phase,"2D · THE GREAT CIRCLE",0.45,smooth);
say(narration,"The surviving cross-section is a circle of radius 3.",0.55,smooth);
say(principle,"The geo kit binds the circle to centre O and radius point R.",0.50,smooth);
rewrite(formula,`x^2+z^2=9\quad\Longleftrightarrow\quad r=\textcolor{gold}{3}`,1.00,smooth);
fade(greatCircle,0.85);
draw(section,1.30,smooth);
}
par { show(O,0.25); show(O.label,0.25); show(A,0.25); show(A.label,0.25); draw(radius2d,0.85,smooth); }
par { pulse(section,0.75); pulse(radius2d,0.75); }
wait(0.70);
}
}
step("a line finds the diameter") {
seq {
par {
say(phase,"2D → 1D · INTERSECT WITH A LINE",0.45,smooth);
say(narration,"A line through the centre meets the circle at exactly two points.",0.55,smooth);
say(principle,"The constructed segment AB is the circle's widest chord.",0.50,smooth);
rewrite(formula,`AB=2r=2(3)`,0.95,smooth);
}
par { show(ends0,0.25); show(ends1,0.25); show(ends0.label,0.25); show(ends1.label,0.25); draw(diameter,1.35,smooth); }
par { fade(radius2d,0.45); fade(A,0.35); fade(A.label,0.35); show(M,0.30); show(M.label,0.30); pulse(M,0.70); }
wait(0.70);
}
}
step("only one measurable direction remains") {
seq {
par {
show(takeaway,0.45);
say(phase,"1D · THE DIAMETER",0.50,smooth);
say(narration,"Remove the surrounding circle. One segment—and one length—remains.",0.55,smooth);
say(principle,"3D sphere → 2D section → 1D diameter",0.55,smooth);
rewrite(formula,`\boxed{AB=\textcolor{gold}{6}\text{ units}}`,1.05,smooth);
fade(section,0.75); fade(O,0.40); fade(O.label,0.40);
}
par { pulse(diameter,0.90); pulse(formula,0.90); }
wait(2.00);
}
}
textbook-watermelon-sections
A paper-style spatial lesson that turns sphere sections into a continuous story: horizontal and vertical great-circle cuts make two halves, then perpendicular cuts separate one quarter from the three-quarter remainder. The section faces are bounded parametric surfaces, so the authored geometry is exact rather than a flat overlay.
// ============================================================================
// textbook-watermelon-sections.manic
// Animated reconstruction of a textbook sketch: horizontal and vertical
// sphere sections, then two perpendicular cuts producing 1/4 + 3/4.
// ============================================================================
title("Watermelon Sections — Slicing a Sphere");
canvas("9:16");
template("paper");
watermark(manicMark,(220,175),"Made With Manic");
creator(me,"@anish2good name=Manic_Geometry tagline=Textbooks_in_motion yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=social safe=reels");
socials(me);
text(kicker,(540,164),"SECTIONS OF A SPHERE"); size(kicker,21); bold(kicker); color(kicker,cyan); hidden(kicker);
text(headline,(540,228),"How a watermelon becomes ½, ¼, and ¾"); size(headline,36); bold(headline); hidden(headline);
text(phase,(540,300),"THE TWO GREAT CIRCLES"); size(phase,23); bold(phase); color(phase,gold); hidden(phase);
text(narration,(540,1328),"A central plane cuts a sphere in a great circle."); size(narration,26); bold(narration); wrap(narration,850); hidden(narration);
text(principle,(540,1392),"Horizontal and vertical sections have the same radius."); size(principle,20); color(principle,dim); hidden(principle);
equation(formula,(540,1488),`x^2+y^2+z^2=R^2`,43); hidden(formula);
text(result,(540,1572),"ONE SPHERE · TWO PERPENDICULAR SECTION PLANES"); size(result,18); bold(result); color(result,gold); hidden(result);
// Stable isometric framing in the portrait Creator media region.
camera3((40.05,-39.35,23.41),(3.15,-2.45,-6.79),40);
// Shared textbook guides: the horizontal ring lies in z=0 and the vertical
// ring lies in y=0. Each chapter owns a duplicate so transitions stay simple.
// --------------------------------------------------------------------------
// Chapter 1 — horizontal cut, two hemispheres
// --------------------------------------------------------------------------
sphere3(hGuide,(0,0,0),3); color(hGuide,blue); hidden(hGuide); tag(hGuide,hScene);
finish3(hGuide,"material=glass shading=smooth mesh=0.18 depth=0.28 shadow=0.12");
curve3(hRing,"3*cos(t)","3*sin(t)","0",(0,6.2832)); color(hRing,cyan); thick(hRing,0.055); untraced(hRing); hidden(hRing); tag(hRing,hScene);
curve3(hVerticalRing,"3*cos(t)","0","3*sin(t)",(0,6.2832)); color(hVerticalRing,dim); thick(hVerticalRing,0.035); untraced(hVerticalRing); hidden(hVerticalRing); tag(hVerticalRing,hScene);
cube3(hPlane,(0,0,4.5),(7,7,0.08)); color(hPlane,cyan); hidden(hPlane); tag(hPlane,hScene);
finish3(hPlane,"material=glass shading=flat mesh=0.08 depth=0.14 shadow=0.04");
param3(hTopShell,
"3*sin(u)*cos(v)","3*sin(u)*sin(v)","3*cos(u)",
(0,1.5708),(0,6.2832),34);
color(hTopShell,lime); hidden(hTopShell); tag(hTopShell,hTop); tag(hTopShell,hScene);
finish3(hTopShell,"material=glass shading=smooth mesh=0.12 depth=0.24 shadow=0.10");
param3(hBottomShell,
"3*sin(u)*cos(v)","3*sin(u)*sin(v)","3*cos(u)",
(1.5708,3.1416),(0,6.2832),34);
color(hBottomShell,lime); hidden(hBottomShell); tag(hBottomShell,hBottom); tag(hBottomShell,hScene);
finish3(hBottomShell,"material=glass shading=smooth mesh=0.12 depth=0.24 shadow=0.10");
param3(hTopFace,"3*u*cos(v)","3*u*sin(v)","0",(0,1),(0,6.2832),30);
color(hTopFace,magenta); hidden(hTopFace); tag(hTopFace,hTop); tag(hTopFace,hScene);
finish3(hTopFace,"material=glass shading=flat mesh=0.30 depth=0.22 shadow=0.08");
param3(hBottomFace,"3*u*cos(v)","3*u*sin(v)","0",(0,1),(0,6.2832),30);
color(hBottomFace,magenta); hidden(hBottomFace); tag(hBottomFace,hBottom); tag(hBottomFace,hScene);
finish3(hBottomFace,"material=glass shading=flat mesh=0.30 depth=0.22 shadow=0.08");
// --------------------------------------------------------------------------
// Chapter 2 — vertical cut, front and back hemispheres
// --------------------------------------------------------------------------
sphere3(vGuide,(0,0,0),3); color(vGuide,blue); hidden(vGuide); tag(vGuide,vScene);
finish3(vGuide,"material=glass shading=smooth mesh=0.18 depth=0.28 shadow=0.12");
curve3(vRing,"3*cos(t)","0","3*sin(t)",(0,6.2832)); color(vRing,lime); thick(vRing,0.055); untraced(vRing); hidden(vRing); tag(vRing,vScene);
curve3(vHorizontalRing,"3*cos(t)","3*sin(t)","0",(0,6.2832)); color(vHorizontalRing,dim); thick(vHorizontalRing,0.035); untraced(vHorizontalRing); hidden(vHorizontalRing); tag(vHorizontalRing,vScene);
cube3(vPlane,(0,-4.5,0),(7,0.08,7)); color(vPlane,lime); hidden(vPlane); tag(vPlane,vScene);
finish3(vPlane,"material=glass shading=flat mesh=0.08 depth=0.14 shadow=0.04");
param3(vFrontShell,
"3*sin(u)*cos(v)","3*sin(u)*sin(v)","3*cos(u)",
(0,3.1416),(3.1416,6.2832),34);
color(vFrontShell,lime); hidden(vFrontShell); tag(vFrontShell,vFront); tag(vFrontShell,vScene);
finish3(vFrontShell,"material=glass shading=smooth mesh=0.12 depth=0.24 shadow=0.10");
param3(vBackShell,
"3*sin(u)*cos(v)","3*sin(u)*sin(v)","3*cos(u)",
(0,3.1416),(0,3.1416),34);
color(vBackShell,lime); hidden(vBackShell); tag(vBackShell,vBack); tag(vBackShell,vScene);
finish3(vBackShell,"material=glass shading=smooth mesh=0.12 depth=0.24 shadow=0.10");
param3(vFrontFace,"3*u*cos(v)","0","3*u*sin(v)",(0,1),(0,6.2832),30);
color(vFrontFace,cyan); hidden(vFrontFace); tag(vFrontFace,vFront); tag(vFrontFace,vScene);
finish3(vFrontFace,"material=glass shading=flat mesh=0.30 depth=0.22 shadow=0.08");
param3(vBackFace,"3*u*cos(v)","0","3*u*sin(v)",(0,1),(0,6.2832),30);
color(vBackFace,cyan); hidden(vBackFace); tag(vBackFace,vBack); tag(vBackFace,vScene);
finish3(vBackFace,"material=glass shading=flat mesh=0.30 depth=0.22 shadow=0.08");
// --------------------------------------------------------------------------
// Chapter 3 — perpendicular cuts, quarter removed and three quarters remain
// --------------------------------------------------------------------------
sphere3(qGuide,(0,0,0),3); color(qGuide,blue); hidden(qGuide); tag(qGuide,qScene);
finish3(qGuide,"material=glass shading=smooth mesh=0.18 depth=0.28 shadow=0.12");
curve3(qHRing,"3*cos(t)","3*sin(t)","0",(0,6.2832)); color(qHRing,cyan); thick(qHRing,0.045); untraced(qHRing); hidden(qHRing); tag(qHRing,qScene);
curve3(qVRing,"3*cos(t)","0","3*sin(t)",(0,6.2832)); color(qVRing,lime); thick(qVRing,0.045); untraced(qVRing); hidden(qVRing); tag(qVRing,qScene);
cube3(qHPlane,(0,0,4.5),(7,7,0.07)); color(qHPlane,cyan); hidden(qHPlane); tag(qHPlane,qScene);
cube3(qVPlane,(0,-4.5,0),(7,0.07,7)); color(qVPlane,lime); hidden(qVPlane); tag(qVPlane,qScene);
finish3(qHPlane,"material=glass shading=flat mesh=0.06 depth=0.12 shadow=0.03");
finish3(qVPlane,"material=glass shading=flat mesh=0.06 depth=0.12 shadow=0.03");
// Removed top-front quarter: one spherical patch plus two semicircular faces.
param3(qShell,
"3*sin(u)*cos(v)","3*sin(u)*sin(v)","3*cos(u)",
(0,1.5708),(3.1416,6.2832),32);
color(qShell,lime); hidden(qShell); tag(qShell,quarterPiece); tag(qShell,qScene);
finish3(qShell,"material=glass shading=smooth mesh=0.14 depth=0.26 shadow=0.12");
param3(qHFace,"3*u*cos(v)","3*u*sin(v)","0",(0,1),(3.1416,6.2832),28);
color(qHFace,gold); hidden(qHFace); tag(qHFace,quarterPiece); tag(qHFace,qScene);
finish3(qHFace,"material=glass shading=flat mesh=0.28 depth=0.22 shadow=0.08");
param3(qVFace,"3*u*cos(v)","0","3*u*sin(v)",(0,1),(0,3.1416),28);
color(qVFace,gold); hidden(qVFace); tag(qVFace,quarterPiece); tag(qVFace,qScene);
finish3(qVFace,"material=glass shading=flat mesh=0.28 depth=0.22 shadow=0.08");
// Remaining 3/4: top-back quarter plus the complete bottom hemisphere.
param3(rTopBack,
"3*sin(u)*cos(v)","3*sin(u)*sin(v)","3*cos(u)",
(0,1.5708),(0,3.1416),32);
color(rTopBack,lime); hidden(rTopBack); tag(rTopBack,remainder); tag(rTopBack,qScene);
finish3(rTopBack,"material=glass shading=smooth mesh=0.14 depth=0.26 shadow=0.12");
param3(rBottom,
"3*sin(u)*cos(v)","3*sin(u)*sin(v)","3*cos(u)",
(1.5708,3.1416),(0,6.2832),34);
color(rBottom,lime); hidden(rBottom); tag(rBottom,remainder); tag(rBottom,qScene);
finish3(rBottom,"material=glass shading=smooth mesh=0.14 depth=0.26 shadow=0.12");
param3(rHFace,"3*u*cos(v)","3*u*sin(v)","0",(0,1),(3.1416,6.2832),28);
color(rHFace,gold); hidden(rHFace); tag(rHFace,remainder); tag(rHFace,qScene);
finish3(rHFace,"material=glass shading=flat mesh=0.28 depth=0.22 shadow=0.08");
param3(rVFace,"3*u*cos(v)","0","3*u*sin(v)",(0,1),(0,3.1416),28);
color(rVFace,gold); hidden(rVFace); tag(rVFace,remainder); tag(rVFace,qScene);
finish3(rVFace,"material=glass shading=flat mesh=0.28 depth=0.22 shadow=0.08");
step("read the two central sections") {
seq {
par { show(kicker,0.35); show(headline,0.55); show(phase,0.40); show(narration,0.45); show(principle,0.40); show(formula,0.55); show(result,0.45); }
to(hGuide,opacity,0.28,0.80,smooth);
par { show(hRing,0.25); draw(hRing,1.20,smooth); show(hVerticalRing,0.25); draw(hVerticalRing,1.20,smooth); }
par { pulse(hRing,0.70); pulse(hVerticalRing,0.70); roll3(3,1.30,smooth); }
wait(0.65);
}
}
step("horizontal cut makes two halves") {
seq {
par {
say(phase,"HORIZONTAL SECTION",0.45,smooth);
say(narration,"Slide a horizontal plane through the centre: z=0.",0.55,smooth);
say(principle,"The intersection is a horizontal great circle.",0.50,smooth);
rewrite(formula,`z=0\quad\Longrightarrow\quad x^2+y^2=R^2`,1.00,smooth);
say(result,"CUT → TWO ½-WATERMELONS",0.45,smooth);
show(hPlane,0.30);
}
par { shift3(hPlane,(0,0,-4.5),2.00,smooth); pulse(hRing,0.75); }
par {
fade(hGuide,0.55); fade(hPlane,0.45); fade(hRing,0.45); fade(hVerticalRing,0.45);
show(hTopShell,0.40); show(hBottomShell,0.40);
to(hTopFace,opacity,0.74,0.50,smooth); to(hBottomFace,opacity,0.74,0.50,smooth);
}
par { shift3(hTop,(-3.8,0,0.7),1.85,smooth); shift3(hBottom,(1.5,0,-0.2),1.85,smooth); }
par { pulse(hTopFace,0.80); pulse(hBottomFace,0.80); }
wait(0.75);
}
}
step("vertical cut makes two halves") {
seq {
par {
fade(hScene,0.65);
say(phase,"VERTICAL SECTION",0.45,smooth);
say(narration,"Turn the cutting plane upright and pass it through y=0.",0.55,smooth);
say(principle,"Its section is the same-size circle, seen vertically.",0.50,smooth);
rewrite(formula,`y=0\quad\Longrightarrow\quad x^2+z^2=R^2`,1.00,smooth);
say(result,"ANOTHER CUT → TWO ½-WATERMELONS",0.45,smooth);
}
par { to(vGuide,opacity,0.26,0.70,smooth); show(vHorizontalRing,0.25); draw(vHorizontalRing,0.90,smooth); show(vRing,0.25); draw(vRing,1.10,smooth); }
par { show(vPlane,0.30); shift3(vPlane,(0,4.5,0),2.00,smooth); pulse(vRing,0.75); }
par {
fade(vGuide,0.55); fade(vPlane,0.45); fade(vRing,0.45); fade(vHorizontalRing,0.45);
show(vFrontShell,0.40); show(vBackShell,0.40);
to(vFrontFace,opacity,0.74,0.50,smooth); to(vBackFace,opacity,0.74,0.50,smooth);
}
par { shift3(vFront,(-3.8,0,0.4),1.85,smooth); shift3(vBack,(1.5,0,-0.1),1.85,smooth); }
wait(0.75);
}
}
step("perpendicular cuts remove one quarter") {
seq {
par {
fade(vScene,0.65);
say(phase,"TWO PERPENDICULAR CUTS",0.45,smooth);
say(narration,"Now combine the horizontal and vertical central planes.",0.55,smooth);
say(principle,"Each cut halves one independent direction.",0.50,smooth);
rewrite(formula,`\frac12\times\frac12=\textcolor{gold}{\frac14}`,1.00,smooth);
say(result,"¼ CUT OUT · ¾ REMAINS",0.45,smooth);
}
par { to(qGuide,opacity,0.24,0.65,smooth); show(qHRing,0.25); draw(qHRing,0.90,smooth); show(qVRing,0.25); draw(qVRing,0.90,smooth); }
seq {
par { show(qHPlane,0.25); shift3(qHPlane,(0,0,-4.5),1.35,smooth); }
par { show(qVPlane,0.25); shift3(qVPlane,(0,4.5,0),1.35,smooth); }
}
par {
fade(qGuide,0.50); fade(qHPlane,0.40); fade(qVPlane,0.40); fade(qHRing,0.40); fade(qVRing,0.40);
show(qShell,0.35); to(qHFace,opacity,0.78,0.45,smooth); to(qVFace,opacity,0.78,0.45,smooth);
show(rTopBack,0.35); show(rBottom,0.35); to(rHFace,opacity,0.70,0.45,smooth); to(rVFace,opacity,0.70,0.45,smooth);
}
par { shift3(quarterPiece,(-3.8,0,0.8),1.90,smooth); shift3(remainder,(1.4,0,-0.15),1.90,smooth); }
wait(0.70);
}
}
step("compare the pieces") {
seq {
par {
say(phase,"FRACTIONS ARE GEOMETRY",0.50,smooth);
say(narration,"The removed corner is one quarter; the connected remainder is three quarters.",0.55,smooth);
say(principle,"Horizontal section · vertical section · quarter volume",0.55,smooth);
rewrite(formula,`1-\frac14=\textcolor{gold}{\frac34}`,1.00,smooth);
show(result,0.25); say(result,"¼ PIECE + ¾ WATERMELON = 1 WHOLE",0.50,smooth);
}
par { pulse(quarterPiece,0.90); pulse(remainder,0.90); roll3(3,1.50,smooth); }
wait(2.00);
}
}
hotdog-taco
A topology lesson disguised as a proof that a hotdog is a taco: the two hallmarks of
textbook rigour — a stated \textbf{Theorem}, a \blacksquare QED, the homeomorphism
DEFINITION ($f, f^{-1}$ continuous) and the genus INVARIANT ($g(\text{bagel})=1\ne0=g(\text{taco})$,
$\chi=2-2g$) — carried by live geometry. Act 1 morphs a 2D blob to prove distance is
negotiable but holes are not; act 2 turns ONE parameter knob to fold a flat param3
tortilla (arc-length preserved — a real homeomorphism, run forwards AND backwards) into
taco then hotdog while a camera orbits; the coda parks a genus-1 bagel forever out of reach.
title("A Hotdog Is a Taco");
canvas("16:9");
template("black");
// ================= HUD =================
text(head, (cx, 60), "A Hotdog Is a Taco"); display(head); cursor(head);
caption(sub, "a rigorous proof, with condiments", (cx, 104), 24, dim); hidden(sub);
text(a1, (cx, 148), "Rule of the game: it's all rubber"); size(a1, 28); color(a1, cyan); hidden(a1);
text(a2, (cx, 148), "The deformation"); size(a2, 28); color(a2, gold); hidden(a2);
text(a3, (cx, 148), "The counterexample: the bagel"); size(a3, 28); color(a3, violet); hidden(a3);
text(cap, (cx, h - 42), ""); size(cap, 26);
// THE claim, stated up front - it earns its QED square at the end
equation(eqT, (cx, 200), `\textbf{Theorem.}\;\;\text{hotdog} \;\cong\; \text{taco}`, 42); hidden(eqT); z(eqT, 10);
// textbook callouts: the DEFINITION (act 1) and the INVARIANT (coda) that make
// the proof rigorous, not just tasty
equation(eqHomeo, (cx, 285), `\text{taco}\;\cong\;\text{hotdog}\ \Longleftrightarrow\ \exists\,f,\ f^{-1}\ \text{continuous}`, 28); hidden(eqHomeo); z(eqHomeo, 10);
equation(eqGenus, (cx, 285), `g(\text{bagel})=1\;\ne\;0=g(\text{taco}),\quad \chi = 2-2g`, 28); hidden(eqGenus); z(eqGenus, 10);
// confetti ring for the QED moment
circle(confbox, (cx, 200), 80); hidden(confbox);
particles(conf, confbox, 22, 4, 9, "ring"); color(conf, gold); hidden(conf);
// act wipe
line(wipe1, (-40, 140), (-40, h - 90)); stroke(wipe1, 4); color(wipe1, gold); glow(wipe1, 14);
// ================= act 1 cast: rubber-sheet geometry (2D) =================
circle(blob, (cx, cy + 50), 120); outlined(blob); stroke(blob, 4); color(blob, cyan); untraced(blob); tag(blob, rub);
polygon(btar, (cx + 150, cy + 60), (cx + 90, cy - 60), (cx - 20, cy - 30), (cx - 150, cy - 10), (cx - 130, cy + 120), (cx - 20, cy + 170), (cx + 100, cy + 150));
outlined(btar); hidden(btar);
morph(blob, btar);
text(rule1, (cx - 340, cy + 50), "stretch / bend / squish : ALLOWED"); size(rule1, 26); color(rule1, lime); hidden(rule1); tag(rule1, rub);
text(rule2, (cx + 340, cy + 50), "cut / glue : FORBIDDEN"); size(rule2, 26); color(rule2, red); hidden(rule2); tag(rule2, rub);
// ================= act 2 cast: the 3D kitchen (right-handed, Z-up) =========
camera3((7.8, -8.8, 4.4), (0, 0, 0.7), 42);
grid3(floor, (0, 0, 0), 3, 1); color(floor, dim); opacity(floor, 0.3); hidden(floor);
// the tortilla: a flat disc-like sheet...
param3(sheet, "u", "v", "0", (-1.4, 1.4), (-1.8, 1.8), 40);
color(sheet, gold); finish3(sheet, "shading=smooth depth=0.2");
// ...driven by ONE knob. The fold angle grows with p; arc length is preserved
// (tortillas bend, they do not stretch) - this IS the homeomorphism, live:
parameter(fold, (w - 180, 610), 0.05, 0.05, 1, "fold", 2); hidden(fold);
bind(fold, sheet, formula, "sin(2.2*p*u)/(2.2*p)", "v", "(1-cos(2.2*p*u))/(2.2*p)");
// the filling: one sausage, dropped in from above
param3(saus, "0.34*cos(u)", "v", "2.6+0.34*sin(u)", (0, tau), (-2.2, 2.2), 28);
color(saus, coral); finish3(saus, "shading=smooth"); hidden(saus);
// the unreachable bagel: genus 1, parked stage right
param3(torus, "2.9+(0.85+0.32*cos(v))*cos(u)", "(0.85+0.32*cos(v))*sin(u)", "0.62+0.32*sin(v)", (0, tau), (0, tau), 32);
color(torus, violet); finish3(torus, "shading=smooth"); hidden(torus);
// pinned labels that live in the 3D world
text(ltaco, (cx, cy), "TACO"); size(ltaco, 32); color(ltaco, lime); hidden(ltaco);
pin3(ltaco, (0, -2.3, 0.9), (0, -14));
text(lhot, (cx, cy), "HOTDOG"); size(lhot, 32); color(lhot, coral); hidden(lhot);
pin3(lhot, (0, -2.3, 0.9), (0, -14));
text(g0, (cx, cy), "genus 0 - no holes"); size(g0, 24); color(g0, lime); hidden(g0);
pin3(g0, (-1.7, 0, 1.9), (0, -12));
text(g1, (cx, cy), "genus 1 - one hole"); size(g1, 24); color(g1, violet); hidden(g1);
pin3(g1, (2.9, 0, 1.6), (0, -14));
// ================= timeline =================
// ---- cold open: state the theorem with a straight face
type(head, 1.1);
wordpop(sub, 0.06);
show(eqT, 0.7); cue(tick);
wait(0.9);
// ---- act 1: the rules (rubber-sheet geometry)
cue(whoosh);
show(a1, 0.5);
draw(blob, 1.0);
show(rule1, 0.5); cue(tick);
show(rule2, 0.5); shake(rule2, 0.7);
say(cap, "Topology keeps only what survives stretching - distance is negotiable, holes are not", 0.5);
show(eqHomeo, 0.6);
to(blob, morph, 1, 1.5, smooth);
say(cap, "To a topologist these are the SAME shape - and every move must run backwards too", 0.5);
to(blob, morph, 0, 1.2, smooth);
wait(0.4);
par { shift(wipe1, (w + 80, 0), 0.7, linear); fade(rub, 0.6); fade(a1, 0.5); fade(eqHomeo, 0.5); }
fade(wipe1, 0.2); // park the wipe fully off-stage so it can't streak act 2
// ---- act 2: the deformation (the actual proof)
cue(whoosh);
show(a2, 0.5);
par { show(floor, 0.8); show(sheet, 1.0); }
say(cap, "Exhibit A: the tortilla. A disc - genus 0, no holes", 0.5);
show(g0, 0.5);
show(fold, 0.4);
wait(0.6);
// one knob, turned continuously: disc -> taco
say(cap, "One knob: FOLD. No cutting, no gluing - just bending", 0.5);
par { to(fold, value, 0.55, 2.4, smooth); orbit3(-18, 20, 10.5, 2.4, smooth); }
show(ltaco, 0.5);
flash(sheet, lime);
cue(pop);
show(saus, 0.2);
shift3(saus, (0, 0, -2.2), 0.9, bounce);
say(cap, "Add filling: TACO", 0.4);
wait(0.7);
// keep turning the SAME knob: taco -> hotdog
say(cap, "Keep folding. Nothing tears, nothing sticks", 0.5);
par { to(fold, value, 1.0, 2.6, smooth); orbit3(25, 16, 10.0, 2.6, smooth); fade(ltaco, 0.6); }
show(lhot, 0.5);
flash(sheet, coral);
say(cap, "HOTDOG. Same disc - just more fold", 0.4);
wait(0.6);
// homeomorphism runs both ways: rewind it before their eyes
say(cap, "And back again... a homeomorphism must run both directions", 0.5);
to(fold, value, 0.55, 1.6, smooth);
to(fold, value, 1.0, 1.6, smooth);
// ---- QED
cue(chime);
rewrite(eqT, `\textbf{Theorem.}\;\;\text{hotdog} \;\cong\; \text{taco}\quad\blacksquare`, 0.9);
seq { show(conf, 0.1); burst(conf, 1.4); }
par { orbit3(115, 18, 10.0, 3.0, smooth); }
say(cap, "No cuts, no glue - QED, with mustard", 0.6);
wait(0.8);
// ---- coda: what a hotdog is NOT
cue(whoosh);
show(a3, 0.5); fade(a2, 0.4);
par { show(torus, 0.8); look3((1.3, 0, 0.7), 1.0, smooth); show(g1, 0.6); }
show(eqGenus, 0.6);
say(cap, "The bagel stays forever out of reach: making a hole needs a CUT", 0.5);
say(cap, "Genus is a topological invariant - it can never change under bending", 0.5);
flash(torus, red);
pulse(torus, 0.8);
wait(0.9);
par { fade(torus, 0.7); fade(g1, 0.5); fade(a3, 0.4); fade(eqGenus, 0.5); look3((0, 0, 0.7), 1.0, smooth); }
// ---- end card: the theorem breathes over a slow orbit of lunch
par { breathe(eqT, 3, 0.05, 0, 6); orbit3(160, 14, 10.5, 6, smooth); }
say(cap, "Lunch, rigorously", 0.6);
wait(12.0);
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);
heightmap3
The Grid Kit → 3D bridge: heightmap3(land, grid, "z(x,y,h)") lifts a 2-D grid’s per-cell
state into a surface3-style terrain mesh. A seeded Wave Function Collapse settles a map,
then its walls rise into an island terrain a camera orbits — the grid kit stays entirely
3D-unaware.
// heightmap3 — a Grid Kit → 3D bridge. A 2D grid's per-cell state is lifted into a
// surface3-style terrain mesh. The grid kit stays entirely 3D-unaware; heightmap3
// lives on the 3D side and just reads the grid's cells. Here a seeded Wave Function
// Collapse settles a map, then its walls rise into an island terrain a camera orbits.
title("Heightmap3 — A Grid Becomes Terrain");
canvas("16:9");
template("neon");
// A grid-kit map: `collapse` pre-simulates a seeded WFC settle (clustered walls).
grid(mz, (cx, cy), 18, 14, 30);
collapse(mz, "islands", 5);
hidden(mz.cells); hidden(mz.lines); // we only want the 3D terrain, not the flat grid
// Lift it: h = 1 for a filled cell, 0 otherwise. The formula raises the walls and
// blends in organic `fbm` noise for a natural roll — grid value AND noise compose.
camera3((0, -12, 7), (0, 0, 0.7), 32);
heightmap3(land, mz, "h*1.5 + fbm(x*1.1, y*1.1)*0.85 + 0.1", 7);
color(land, cyan);
// A slow turntable so the terrain reads in 3D.
orbit3(70, 34, 13, 4.5, smooth);
orbit3(200, 30, 13, 4.5, smooth);
orbit3(330, 36, 13, 4.5, smooth);
heightmap3-world
The creative payoff: a grid-kit WFC map settles in 2-D, then the very same grid lifts into
a 3-D world as the camera tilts down — one grid, two dimensions. h (the cell value) is a
third formula variable added to the expression engine for exactly this.
// From Flat Map to 3D World — the creative payoff of the Grid→3D bridge. A grid-kit
// Wave Function Collapse settles a map in 2D; then the very same grid is lifted by
// heightmap3 into a 3D terrain that rises as the camera tilts down. One grid, two
// dimensions — the grid kit never learns about 3D.
title("From Flat Map to 3D World");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mk, (w*0.86, h*0.06), "Made With Manic");
// The 2D map (grid kit): a seeded WFC settle.
grid(world, (cx, cy), 20, 16, 30);
collapse(world, "maze", 11);
// The 3D terrain from the SAME grid — near top-down at first, so it reads flat like
// the map, then the tilt reveals its height. Hidden until the lift.
camera3((0, -1.6, 20), (0, 0, 0), 24);
heightmap3(land, world, "h*1.9 + fbm(x*1.2, y*1.2)*0.9 + 0.1", 8);
color(land, cyan);
hidden(land);
text(cap, (cx, h*0.9), "a flat 2D map…");
size(cap, 30*u); color(cap, dim); wrap(cap, w*0.7);
// 1 — the map settles in 2D (Wave Function Collapse).
step("settle") {
par {
say(cap, "a flat 2D map, generated cell by cell…");
run(world, 16, 3.4);
}
}
wait(0.4);
// 2 — the same grid lifts into 3D: fade the flat map, reveal the terrain (still
// near top-down, so it still reads flat — the map has just gained a third axis).
step("lift") {
par {
say(cap, "…is secretly a 3D world");
fade(world.cells, 0.6);
fade(world.lines, 0.6);
show(land, 0.7);
}
}
// 3 — pull back and tilt so the WHOLE world is in frame at once.
step("reveal") {
orbit3(25, 38, 27, 3.8, smooth);
}
wait(0.4);
// 4 — a slow rotation around the full terrain.
step("rotate") {
seq {
say(cap, "one grid — 2D pathfinding AND 3D terrain, bridged by manic");
orbit3(170, 34, 27, 5.2, smooth);
}
}
// 5 — then fly in close and low over the peaks. That's manic.
step("zoom") {
orbit3(235, 15, 10, 4.6, smooth);
}
wait(1.2);
noise-terrain
Procedural generation from a single formula: noise(x,y) and fbm(x,y) (fractal Brownian
motion) are now formula functions beside sin/cos, so surface3(land, "fbm(x*0.9,y*0.9)*2.4")
sculpts an organic fractal landscape the camera tours — no new kit, just two functions the
shared expression evaluator now understands.
// Fractal Terrain — procedural generation from a single formula. `noise(x,y)` and
// `fbm(x,y)` (fractal Brownian motion — five octaves of smooth value noise) are now
// formula functions, right beside sin/cos, so any `surface3`/`heightmap3`/`plot`
// formula can be organic. Here fbm alone sculpts a whole landscape the camera tours.
title("Fractal Terrain — Noise in a Formula");
canvas("16:9");
template("neon");
camera3((0, -13, 6.5), (0, 0, 0.3), 28);
surface3(land, "fbm(x*0.9, y*0.9)*2.4", (-4, 4), (-4, 4), 72);
color(land, cyan);
// A slow turntable so the fractal relief reads in 3D.
orbit3(60, 30, 13, 4.5, smooth);
orbit3(190, 26, 13, 4.5, smooth);
orbit3(320, 32, 13, 4.5, smooth);
Diagrams
Animated diagrams — not another static boxes-and-arrows generator. Declare an architecture (or a flowchart) with auto-positioned nodes and directed connections — geometry is optional, so a diagram auto-fits the canvas and, when it grows dense, scales itself down as one to stay inside the frame (you never touch a coordinate) — then move one persistent request through the graph. A flowchart ranks its nodes top-down and runs: a token walks the process and takes a branch. It also speaks C4 — c4(id, level) with person/system/container/component nodes draws Simon Brown’s model in the conventional outline style, tiers people over internals over externals, auto-splits a dense tier into a balanced grid, and zooms from Context to Container to Component as a request flows through it. Node artwork comes from a string kind (aws:lambda, gcp:bigquery, onprem:redis, k8s:pod — 17 providers, see icon reference & aliases); paths are coloured by relationship. The kit never infers behaviour — the creator authors it with route, flow, and ordinary verbs.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
systems-foundation
Provider-neutral: only native client/gateway/service/cache/database archetypes — reveals cold topology, sends one persistent request forward, and returns the same identity over separately directed paths, with no cloud assets.
title("Systems Foundation — One Request, One Return");
canvas("16:9");
template("mono");
watermark(mark, (160, 54), "Made With Manic");
text(kicker, (cx, 54), "PROVIDER-NEUTRAL SYSTEMS");
text(headline, (cx, 104), "Structure is not behaviour");
text(caption, (cx, h-58), "Cold paths show what is possible. One persistent dot shows what actually happened.");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
size(caption, 20); color(caption, dim); wrap(caption, w*0.86);
architecture(platform, (cx, cy+15), w*0.88, h*0.58);
node(user, platform, "client", "User");
node(edge, platform, "gateway", "Gateway");
node(api, platform, "service", "API Service");
node(cache, platform, "cache", "Cache");
node(db, platform, "database", "Database");
connect(toEdge, user, edge);
connect(toApi, edge, api);
connect(toCache, api, cache);
// One semantic connection: Manic chooses node-boundary ports and keeps the
// packet continuous across every internal orthogonal segment.
connect(toDb, cache, db, orthogonal, right, top);
connect(dbReturn, db, api, 95);
connect(apiReturn, api, edge, 75);
connect(edgeReturn, edge, user, 55);
message(packet, user, "GET");
hidden(kicker); hidden(headline); hidden(caption);
untraced(platform.connections);
hidden(packet);
step("structure") {
par {
show(kicker, 0.35);
show(headline, 0.45);
show(caption, 0.45);
draw(platform.connections, 1.20, smooth);
}
}
wait(0.45);
step("continuous-round-trip") {
par {
show(packet, 0.20);
say(caption, "One dot moves continuously end to end. Every route hand-off preserves its identity.", 0.35);
seq {
route(packet, toEdge, 0.70, linear);
route(packet, toApi, 0.70, linear);
route(packet, toCache, 0.70, linear);
route(packet, toDb, 0.70, linear);
par {
recolor(packet, magenta, 0.30);
route(packet, dbReturn, 0.80, linear);
}
route(packet, apiReturn, 0.75, linear);
route(packet, edgeReturn, 0.75, linear);
}
}
}
wait(0.45);
step("proof") {
par {
pulse(packet, 0.70);
say(caption, "The same dot is back at the User: one identity, seven connected journeys, zero animation gaps.", 0.40);
}
}
wait(1.10);
systems-architecture-poc
The first story: one Buy request travels Browser → CloudFront → API Gateway → Lambda → DynamoDB → SQS, auto-laid horizontally or vertically by the responsive region.
title("Systems Kit PoC — What Happens When You Click Buy?");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.07), "Made With Manic");
text(kicker, (cx, h*0.075), "SYSTEMS KIT · REQUEST JOURNEY");
text(headline, (cx, h*0.155), "What happens when you click Buy?");
text(caption, (cx, h*0.88), "One request keeps its identity while the architecture responds.");
size(kicker, 18*u); color(kicker, dim); bold(kicker);
size(headline, 34*u); bold(headline); wrap(headline, w*0.84);
size(caption, 19*u); color(caption, dim); wrap(caption, w*0.82);
architecture(shop, (cx, h*0.49), w*0.84, h*0.58);
node(browser, shop, "client", "Browser");
node(edge, shop, "aws:cloudfront", "CloudFront");
node(api, shop, "aws:api-gateway", "API Gateway");
node(checkout, shop, "aws:lambda", "Checkout");
node(orders, shop, "aws:dynamodb", "Orders");
node(events, shop, "aws:sqs", "Events");
connect(toEdge, browser, edge);
connect(toApi, edge, api);
connect(toCheckout, api, checkout);
connect(toOrders, checkout, orders);
connect(toEvents, orders, events);
request(order, browser, "BUY");
hidden(shop.nodes);
untraced(shop.connections);
hidden(order.parts);
step("architecture") {
stagger(0.10) {
show(browser, 0.30); show(edge, 0.30); show(api, 0.30);
show(checkout, 0.30); show(orders, 0.30); show(events, 0.30);
}
}
wait(0.25);
step("request") {
par {
show(order.parts, 0.30);
draw(shop.connections, 0.90);
say(caption, "The browser sends one request into the edge network.", 0.35);
}
}
wait(0.20);
step("edge") {
par {
route(order, toEdge, 0.90, smooth);
say(caption, "CloudFront receives it first, close to the customer.", 0.35);
}
}
step("api") {
par {
route(order, toApi, 0.90, smooth);
say(caption, "API Gateway validates and routes the operation.", 0.35);
}
}
step("compute") {
par {
route(order, toCheckout, 0.90, smooth);
say(caption, "Lambda executes the checkout logic.", 0.35);
}
}
step("persist") {
par {
route(order, toOrders, 0.90, smooth);
say(caption, "DynamoDB persists the order.", 0.35);
}
}
step("event") {
par {
route(order, toEvents, 0.90, smooth);
say(caption, "SQS decouples the work that can happen next.", 0.35);
}
}
wait(0.30);
step("takeaway") {
par {
pulse(shop.nodes, 0.75);
say(caption, "Architecture becomes understandable when we follow behavior—not only boxes and arrows.", 0.45);
}
}
wait(1.20);
systems-arrow-patterns
The connection-grammar reference: one-way, parallel, round-trip, port-aware orthogonal, fan-out, and diagonal-duplex lanes.
title("Systems Foundation — Arrow Patterns");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
let pw = w*0.27;
let ph = h*0.25;
let x1 = w*0.18;
let x2 = w*0.50;
let x3 = w*0.82;
let y1 = h*0.34;
let y2 = h*0.68;
watermark(mark, (w*0.09, h*0.045), "Made With Manic");
text(kicker, (cx, h*0.045), "SYSTEMS FOUNDATION · DIRECTION IS VISIBLE");
text(headline, (cx, h*0.12), "One arrow grammar, many delivery stories");
text(caption, (cx, h*0.925), "Arrows describe paths. seq, par, travel, and flow describe what moves through them.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 31*u); bold(headline); wrap(headline, w*0.86);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.88);
rect(p1, (x1,y1), pw,ph); outlined(p1); color(p1, dim); stroke(p1, 2*u);
rect(p2, (x2,y1), pw,ph); outlined(p2); color(p2, dim); stroke(p2, 2*u);
rect(p3, (x3,y1), pw,ph); outlined(p3); color(p3, dim); stroke(p3, 2*u);
rect(p4, (x1,y2), pw,ph); outlined(p4); color(p4, dim); stroke(p4, 2*u);
rect(p5, (x2,y2), pw,ph); outlined(p5); color(p5, dim); stroke(p5, 2*u);
rect(p6, (x3,y2), pw,ph); outlined(p6); color(p6, dim); stroke(p6, 2*u);
text(t1, (x1,y1-ph*0.36), "ONE WAY");
text(t2, (x2,y1-ph*0.36), "ROUND TRIP");
text(t3, (x3,y1-ph*0.36), "ORTHOGONAL");
text(t4, (x1,y2-ph*0.36), "VERTICAL PORTS");
text(t5, (x2,y2-ph*0.36), "FAN OUT");
text(t6, (x3,y2-ph*0.36), "DIAGONAL DUPLEX");
size(t1, 16*u); size(t2, 16*u); size(t3, 16*u);
size(t4, 16*u); size(t5, 16*u); size(t6, 16*u);
color(t1, dim); color(t2, dim); color(t3, dim);
color(t4, dim); color(t5, dim); color(t6, dim);
bold(t1); bold(t2); bold(t3); bold(t4); bold(t5); bold(t6);
// 1 — one source, one destination.
arrow(one, (x1-pw*0.34,y1+ph*0.08), (x1+pw*0.34,y1+ph*0.08));
color(one, cyan); stroke(one, 4*u); untraced(one);
circle(oneDot, (x1-pw*0.34,y1+ph*0.08), 8*u); color(oneDot, cyan); hidden(oneDot);
// 2 — request and response remain separate, honest directions.
arrow(outbound, (x2-pw*0.34,y1), (x2+pw*0.34,y1));
arrow(inbound, (x2+pw*0.34,y1+ph*0.17), (x2-pw*0.34,y1+ph*0.17));
color(outbound, cyan); color(inbound, magenta);
stroke(outbound, 4*u); stroke(inbound, 4*u);
untraced(outbound); untraced(inbound);
circle(requestDot, (x2-pw*0.34,y1), 8*u); color(requestDot, cyan); hidden(requestDot);
circle(responseDot, (x2+pw*0.34,y1+ph*0.17), 8*u); color(responseDot, magenta); hidden(responseDot);
// 3 — generic geometry can draw a stepped path; Systems nodes use one
// `connect(path, a, b, orthogonal)` declaration for the same routed identity.
line(elbowA, (x3-pw*0.34,y1-ph*0.02), (x3,y1-ph*0.02));
line(elbowB, (x3,y1-ph*0.02), (x3,y1+ph*0.20));
arrow(elbowC, (x3,y1+ph*0.20), (x3+pw*0.34,y1+ph*0.20));
color(elbowA, gold); color(elbowB, gold); color(elbowC, gold);
stroke(elbowA, 4*u); stroke(elbowB, 4*u); stroke(elbowC, 4*u);
untraced(elbowA); untraced(elbowB); untraced(elbowC);
circle(elbowDot, (x3-pw*0.34,y1-ph*0.02), 8*u); color(elbowDot, gold); hidden(elbowDot);
// 4 — ports can face vertically without changing motion semantics.
arrow(downLane, (x1-pw*0.12,y2-ph*0.20), (x1-pw*0.12,y2+ph*0.25));
arrow(upLane, (x1+pw*0.12,y2+ph*0.25), (x1+pw*0.12,y2-ph*0.20));
color(downLane, cyan); color(upLane, magenta);
stroke(downLane, 4*u); stroke(upLane, 4*u);
untraced(downLane); untraced(upLane);
circle(downDot, (x1-pw*0.12,y2-ph*0.20), 8*u); color(downDot, cyan); hidden(downDot);
circle(upDot, (x1+pw*0.12,y2+ph*0.25), 8*u); color(upDot, magenta); hidden(upDot);
// 5 — fan-out is three explicitly authored deliveries, not inferred broadcast.
arrow(fanA, (x2-pw*0.34,y2+ph*0.04), (x2+pw*0.34,y2-ph*0.22));
arrow(fanB, (x2-pw*0.34,y2+ph*0.04), (x2+pw*0.34,y2+ph*0.04));
arrow(fanC, (x2-pw*0.34,y2+ph*0.04), (x2+pw*0.34,y2+ph*0.30));
color(fanA, cyan); color(fanB, cyan); color(fanC, cyan);
stroke(fanA, 3*u); stroke(fanB, 3*u); stroke(fanC, 3*u);
untraced(fanA); untraced(fanB); untraced(fanC);
circle(fanDotA, (x2-pw*0.34,y2+ph*0.04), 7*u); color(fanDotA, cyan); hidden(fanDotA);
circle(fanDotB, (x2-pw*0.34,y2+ph*0.04), 7*u); color(fanDotB, cyan); hidden(fanDotB);
circle(fanDotC, (x2-pw*0.34,y2+ph*0.04), 7*u); color(fanDotC, cyan); hidden(fanDotC);
// 6 — diagonal request/return lanes use different styling and directions.
arrow(diagOut, (x3-pw*0.32,y2+ph*0.24), (x3+pw*0.27,y2-ph*0.22));
arrow(diagBack, (x3+pw*0.34,y2-ph*0.12), (x3-pw*0.25,y2+ph*0.34));
color(diagOut, cyan); color(diagBack, magenta); dashed(diagBack);
stroke(diagOut, 4*u); stroke(diagBack, 4*u);
untraced(diagOut); untraced(diagBack);
circle(diagRequest, (x3-pw*0.32,y2+ph*0.24), 8*u); color(diagRequest, cyan); hidden(diagRequest);
circle(diagResponse, (x3+pw*0.34,y2-ph*0.12), 8*u); color(diagResponse, magenta); hidden(diagResponse);
tag(p1, cards); tag(p2, cards); tag(p3, cards); tag(p4, cards); tag(p5, cards); tag(p6, cards);
tag(t1, cards); tag(t2, cards); tag(t3, cards); tag(t4, cards); tag(t5, cards); tag(t6, cards);
tag(one, paths); tag(outbound, paths); tag(inbound, paths);
tag(elbowA, paths); tag(elbowB, paths); tag(elbowC, paths);
tag(downLane, paths); tag(upLane, paths);
tag(fanA, paths); tag(fanB, paths); tag(fanC, paths);
tag(diagOut, paths); tag(diagBack, paths);
hidden(kicker); hidden(headline); hidden(caption); hidden(cards);
step("arrow-grammar") {
par {
show(kicker, 0.30);
show(headline, 0.45);
show(caption, 0.40);
show(cards, 0.45);
draw(paths, 1.10, smooth);
}
}
wait(0.35);
step("traffic") {
par {
show(oneDot, 0.15); travel(oneDot, one, 1.20, linear); flow(one, 1.20);
seq {
show(requestDot, 0.15); travel(requestDot, outbound, 0.65, linear);
show(responseDot, 0.15); travel(responseDot, inbound, 0.65, linear);
}
seq {
show(elbowDot, 0.15);
travel(elbowDot, elbowA, 0.40, linear);
travel(elbowDot, elbowB, 0.35, linear);
travel(elbowDot, elbowC, 0.45, linear);
}
par {
show(downDot, 0.15); travel(downDot, downLane, 1.20, linear);
show(upDot, 0.15); travel(upDot, upLane, 1.20, linear);
}
par {
show(fanDotA, 0.15); travel(fanDotA, fanA, 1.20, linear);
show(fanDotB, 0.15); travel(fanDotB, fanB, 1.20, linear);
show(fanDotC, 0.15); travel(fanDotC, fanC, 1.20, linear);
}
par {
show(diagRequest, 0.15); travel(diagRequest, diagOut, 1.20, linear);
show(diagResponse, 0.15); travel(diagResponse, diagBack, 1.20, linear);
}
}
}
wait(0.55);
step("creator-controls-semantics") {
par {
pulse(paths, 0.75);
say(caption, "The same paths can mean request, response, queue delivery, broadcast, or pure motion—the creator decides.", 0.40);
}
}
wait(1.20);
microservices-platform
Auto-fit showcase: Route 53 → load balancer → gateway, three availability zones of ECS services, a replicated RDS database cluster, and CloudWatch monitoring — declared with zero coordinates. Add a tier or a zone and the whole diagram reflows and scales itself down to the frame; paths coloured by relationship (request · write · replication · telemetry).
title("A Microservices Platform — Laid Out Automatically");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic");
text(kicker, (cx, h*0.055), "DIAGRAMS · AUTO-FIT · NO COORDINATES");
text(headline, (cx, h*0.12), "Add a tier. It re-fits itself.");
text(caption, (cx, h*0.95), "DNS, load balancing, three AZs of services, a database cluster, and monitoring — all auto-laid.");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 26*u); bold(headline); wrap(headline, w*0.86);
size(caption, 17*u); color(caption, dim); wrap(caption, w*0.86);
// No geometry: the diagram auto-fits. Route 53, an edge load balancer, three
// availability zones, a replicated database cluster and monitoring all reflow
// to fit — and when the content would overflow, the whole diagram scales down
// as one. The author types no coordinate.
architecture(platform);
node(dns, platform, "aws:route53", "route 53");
node(lb, platform, "aws:elb", "load balancer");
node(gateway, platform, "aws:api-gateway", "gateway");
cluster(region, platform, "REGION us-east-1");
cluster(azA, region, "AZ-A");
node(a1, azA, "aws:ecs", "svc"); node(a2, azA, "aws:ecs", "svc"); node(a3, azA, "aws:ecs", "svc");
cluster(azB, region, "AZ-B");
node(b1, azB, "aws:ecs", "svc"); node(b2, azB, "aws:ecs", "svc"); node(b3, azB, "aws:ecs", "svc");
cluster(azC, region, "AZ-C");
node(c1, azC, "aws:ecs", "svc"); node(c2, azC, "aws:ecs", "svc"); node(c3, azC, "aws:ecs", "svc");
cluster(database, platform, "DATABASE CLUSTER");
node(primary, database, "aws:rds", "primary");
node(replica1, database, "aws:rds", "replica");
node(replica2, database, "aws:rds", "replica");
node(cache, database, "aws:elasticache", "cache");
cluster(observability, platform, "MONITORING");
node(metrics, observability, "aws:cloudwatch", "metrics");
node(logs, observability, "aws:cloudwatch", "logs");
node(alarms, observability, "aws:cloudwatch", "alarms");
connect(dnsToLb, dns, lb); // resolve, then hit the edge load balancer
connect(lbToGw, lb, gateway); // balanced across the API gateway
connect(toA, gateway, azA); // gateway fans out to each zone's services
connect(toB, gateway, azB);
connect(toC, gateway, azC);
connect(toWrite, a1, primary); // a representative service writes the primary
connect(toRead, a1, replica1); // reads served from a replica
connect(toCache, a1, cache);
connect(repl1, primary, replica1); // primary replicates to both replicas
connect(repl2, primary, replica2);
connect(toMetrics, b1, metrics); // services emit telemetry to monitoring
color(dnsToLb, cyan); color(dnsToLb.hot, cyan); // request path
color(lbToGw, cyan); color(lbToGw.hot, cyan);
color(toA, cyan); color(toA.hot, cyan);
color(toB, cyan); color(toB.hot, cyan);
color(toC, cyan); color(toC.hot, cyan);
color(toRead, cyan); color(toRead.hot, cyan);
color(toCache, cyan); color(toCache.hot, cyan);
color(toWrite, magenta); color(toWrite.hot, magenta); // write path
color(repl1, lime); color(repl1.hot, lime); // replication
color(repl2, lime); color(repl2.hot, lime);
color(toMetrics, gold); color(toMetrics.hot, gold); // telemetry
request(call, dns, "GET /order");
hidden(platform.nodes);
hidden(region.parts); hidden(azA.parts); hidden(azB.parts); hidden(azC.parts);
hidden(database.parts); hidden(observability.parts);
hidden(call.parts);
untraced(dnsToLb); untraced(lbToGw);
untraced(toA); untraced(toB); untraced(toC);
untraced(toWrite); untraced(toRead); untraced(toCache);
untraced(repl1); untraced(repl2); untraced(toMetrics);
step("edge") {
par {
stagger(0.08) { show(dns, 0.4); show(lb, 0.4); show(gateway, 0.4); }
say(caption, "Route 53 resolves the name, an edge load balancer spreads the traffic, the gateway takes it from there.", 0.4);
}
}
wait(0.2);
step("reveal-zones") {
par {
show(region.parts, 0.4);
show(azA.parts, 0.4); stagger(0.05) { show(a1,0.3); show(a2,0.3); show(a3,0.3); }
show(azB.parts, 0.4); stagger(0.05) { show(b1,0.3); show(b2,0.3); show(b3,0.3); }
show(azC.parts, 0.4); stagger(0.05) { show(c1,0.3); show(c2,0.3); show(c3,0.3); }
say(caption, "Three availability zones, each with its own services — nested clusters, auto-laid.", 0.4);
}
}
wait(0.2);
step("state-and-monitoring") {
par {
show(database.parts, 0.4); show(primary, 0.4); show(replica1, 0.4); show(replica2, 0.4); show(cache, 0.4);
show(observability.parts, 0.4); show(metrics, 0.4); show(logs, 0.4); show(alarms, 0.4);
say(caption, "A replicated database cluster and monitoring join in — the whole diagram re-fits.", 0.4);
}
}
wait(0.25);
step("topology") {
par {
stagger(0.06) {
draw(dnsToLb,0.4); draw(lbToGw,0.4);
draw(toA,0.4); draw(toB,0.4); draw(toC,0.4);
draw(toWrite,0.4); draw(toRead,0.4); draw(toCache,0.4);
draw(repl1,0.4); draw(repl2,0.4); draw(toMetrics,0.4);
}
say(caption, "Paths coloured by relationship: request cyan, writes magenta, replication lime, telemetry gold.", 0.4);
}
}
wait(0.3);
step("one-request") {
par {
seq {
show(call.parts, 0.2);
route(call, dnsToLb, 0.7, smooth);
route(call, lbToGw, 0.7, smooth);
route(call, toA, 0.7, smooth);
route(call, toWrite, 0.7, smooth);
}
say(caption, "One order: Route 53 → load balancer → gateway → an AZ-A service → the primary database.", 0.4);
}
}
wait(0.3);
step("takeaway") {
par {
pulse(region.parts, 0.7);
pulse(database.parts, 0.7);
say(caption, "Add a tier, a zone, a replica — you never touch a coordinate. That is manic.", 0.45);
}
}
wait(1.4);
factorial-flowchart
A flowchart that runs: flowchart(fc) auto-lays seven shape-nodes (terminator/io/process/decision) top-down with no coordinates, then a token walks the factorial loop — taking the yes branch, looping back, and exiting to the end. Node shapes are string kinds; branches are coloured and annotated yes/no/loop, on a clean paper theme with a colour legend.
title("A Flowchart That Runs — the Factorial Loop");
canvas("16:9");
template("paper");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic");
text(kicker, (cx, h*0.06), "DIAGRAMS · FLOWCHART · AUTO-LAID");
text(headline, (cx, h*0.12), "A flowchart that runs");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 26*u); bold(headline); wrap(headline, w*0.86);
// `flowchart(fc)` ranks nodes by their connections, auto-fits, and auto-orients:
// a shallow flow like this stays top-down on its own. No coordinates, no direction
// to choose. Node shapes are string kinds.
flowchart(fc);
node(start, fc, "terminator", "start");
node(rd, fc, "io", "read n");
node(init, fc, "process", "f=1 i=1");
node(dec, fc, "decision", "i <= n?");
node(body, fc, "process", "f=f*i i=i+1");
node(out, fc, "io", "print f");
node(fin, fc, "terminator", "end");
connect(e1, start, rd);
connect(e2, rd, init);
connect(e3, init, dec);
connect(e4, dec, body); // keep looping
connect(e5, dec, out); // exit
connect(e6, body, dec, orthogonal, right, right); // loop back up the side
connect(e7, out, fin);
color(e4, cyan); color(e4.hot, cyan);
color(e5, lime); color(e5.hot, lime);
color(e6, gold); color(e6.hot, gold);
annotate(e4, "yes"); annotate(e5, "no"); annotate(e6, "loop");
// A colour key stands in for narration — the diagram explains itself.
text(k1, (w*0.34, h*0.93), "— continue"); size(k1, 14*u); color(k1, cyan); bold(k1);
text(k2, (w*0.50, h*0.93), "— exit"); size(k2, 14*u); color(k2, lime); bold(k2);
text(k3, (w*0.66, h*0.93), "— loop back"); size(k3, 14*u); color(k3, gold); bold(k3);
request(tok, start, "n = 3");
hidden(fc.nodes);
hidden(tok.parts);
step("shape") {
stagger(0.07) {
show(start, 0.3); show(rd, 0.3); show(init, 0.3); show(dec, 0.3);
show(body, 0.3); show(out, 0.3); show(fin, 0.3);
}
}
wait(0.2);
step("run") {
seq {
show(tok.parts, 0.2);
route(tok, e1, 0.4, smooth); route(tok, e2, 0.4, smooth); route(tok, e3, 0.4, smooth);
route(tok, e4, 0.4, smooth); route(tok, e6, 0.5, smooth);
route(tok, e4, 0.4, smooth); route(tok, e6, 0.5, smooth);
route(tok, e4, 0.4, smooth); route(tok, e6, 0.5, smooth);
route(tok, e5, 0.4, smooth); route(tok, e7, 0.4, smooth);
}
}
wait(0.3);
step("takeaway") {
seq { pulse(fin, 0.7); pulse(fin, 0.7); }
}
wait(1.0);
complex-flowchart
A big pipeline that builds itself, then runs: a 24-node CI/CD flow with 7 decisions and feedback loops auto-wraps into readable side-by-side columns (long loops routed around the perimeter). It first draws box-by-box in flow order, then pairs of commits race the pipeline in parallel to different outcomes — ship, rollback-to-start, held — twice. All flowchart + route + par, no coordinates.
title("Deployment Pipeline — Many Decisions & Loops");
// Stress test: 7 decisions and several back-edges — some loop to an earlier step,
// one loops all the way back to `start`. `flowchart` still auto-wraps into
// side-by-side columns; forward edges are clean elbows, backward edges (loops,
// column wraps) arc as curves. No coordinates.
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic");
text(kicker, (cx, h*0.055), "DIAGRAMS · FLOWCHART · DECISIONS & LOOPS");
text(headline, (cx, h*0.12), "Seven decisions, five loops, one chart");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 23*u); bold(headline); wrap(headline, w*0.86);
flowchart(cd, auto, 40);
node(start, cd, "terminator", "start");
node(checkout, cd, "io", "checkout");
node(install, cd, "process", "install deps");
node(build, cd, "process", "build");
node(bok, cd, "decision", "build ok?");
node(fix, cd, "process", "fix build");
node(unit, cd, "process", "unit tests");
node(uok, cd, "decision", "tests pass?");
node(debug, cd, "process", "debug");
node(integ, cd, "process", "integration");
node(iok, cd, "decision", "integ ok?");
node(scan, cd, "process", "security scan");
node(sok, cd, "decision", "scan clean?");
node(patch, cd, "process", "patch deps");
node(stage, cd, "process", "deploy staging");
node(smoke, cd, "process", "smoke test");
node(smok, cd, "decision", "smoke ok?");
node(approve, cd, "decision", "approve prod?");
node(hold, cd, "terminator", "held");
node(roll, cd, "process", "rollback");
node(prod, cd, "process", "deploy prod");
node(health, cd, "decision", "healthy?");
node(notify, cd, "process", "notify team");
node(done, cd, "terminator", "shipped");
connect(e1, start, checkout);
connect(e2, checkout, install);
connect(e3, install, build);
connect(e4, build, bok);
connect(e5, bok, unit); // yes
connect(e6, bok, fix); // no
connect(e7, fix, build); // loop -> build
connect(e8, unit, uok);
connect(e9, uok, integ); // yes
connect(e10, uok, debug); // no
connect(e11, debug, build); // loop -> build
connect(e12, integ, iok);
connect(e13, iok, scan); // yes
connect(e14, iok, debug); // no -> debug (which loops to build)
connect(e15, scan, sok);
connect(e16, sok, stage); // yes
connect(e17, sok, patch); // no
connect(e18, patch, scan); // loop -> scan
connect(e19, stage, smoke);
connect(e20, smoke, smok);
connect(e21, smok, approve); // yes
connect(e22, smok, roll); // no
connect(e23, roll, start); // loop -> start (all the way back)
connect(e24, approve, prod); // yes
connect(e25, approve, hold); // no
connect(e26, prod, health);
connect(e27, health, notify); // yes
connect(e28, health, roll); // no -> rollback
connect(e29, notify, done);
// forward = proceed, back-edges = loops/failures
color(e6, magenta); color(e6.hot, magenta);
color(e10, magenta); color(e10.hot, magenta);
color(e14, magenta); color(e14.hot, magenta);
color(e17, magenta); color(e17.hot, magenta);
color(e22, magenta); color(e22.hot, magenta);
color(e28, magenta); color(e28.hot, magenta);
color(e7, gold); color(e7.hot, gold);
color(e11, gold); color(e11.hot, gold);
color(e18, gold); color(e18.hot, gold);
color(e23, gold); color(e23.hot, gold);
annotate(e5, "yes"); annotate(e6, "no");
annotate(e9, "yes"); annotate(e10, "no");
annotate(e16, "yes"); annotate(e17, "no");
annotate(e21, "yes"); annotate(e22, "no");
annotate(e24, "yes"); annotate(e25, "no");
annotate(e27, "yes"); annotate(e28, "no");
text(k1, (w*0.36, h*0.94), "— proceed"); size(k1, 13*u); color(k1, fg); bold(k1);
text(k2, (w*0.52, h*0.94), "— reject"); size(k2, 13*u); color(k2, magenta); bold(k2);
text(k3, (w*0.66, h*0.94), "— loop back"); size(k3, 13*u); color(k3, gold); bold(k3);
// Two tokens per round, released together — they share the pipeline, then split
// at a decision and reach different ends. Two rounds, then done.
request(a1, start, "PR-A"); request(b1, start, "PR-B"); // round 1
request(a2, start, "PR-C"); request(b2, start, "PR-D"); // round 2
hidden(cd.nodes);
untraced(cd.connections); // edges start undrawn — we draw them as we build
hidden(a1.parts); hidden(b1.parts); hidden(a2.parts); hidden(b2.parts);
// Build the chart the way you'd draw it: each box appears in flow order and the
// edge into it draws first, so you watch the whole pipeline take shape — then the
// tokens run it. Best way to learn how a flowchart is put together.
step("build-chart") {
seq {
show(start, 0.3);
draw(e1, 0.14); show(checkout, 0.18);
draw(e2, 0.14); show(install, 0.18);
draw(e3, 0.14); show(build, 0.18);
draw(e4, 0.14); show(bok, 0.18);
draw(e5, 0.14); show(unit, 0.18);
draw(e6, 0.14); show(fix, 0.18);
draw(e8, 0.14); show(uok, 0.18);
draw(e9, 0.14); show(integ, 0.18);
draw(e10, 0.14); show(debug, 0.18);
draw(e12, 0.14); show(iok, 0.18);
draw(e13, 0.14); show(scan, 0.18);
draw(e14, 0.14); // iok no → debug (already shown)
draw(e15, 0.14); show(sok, 0.18);
draw(e16, 0.14); show(stage, 0.18);
draw(e17, 0.14); show(patch, 0.18);
draw(e19, 0.14); show(smoke, 0.18);
draw(e20, 0.14); show(smok, 0.18);
draw(e21, 0.14); show(approve, 0.18);
draw(e22, 0.14); show(roll, 0.18);
draw(e24, 0.14); show(prod, 0.18);
draw(e25, 0.14); show(hold, 0.18);
draw(e26, 0.14); show(health, 0.18);
draw(e27, 0.14); show(notify, 0.18);
draw(e28, 0.14); // health no → roll (already shown)
draw(e29, 0.14); show(done, 0.18);
// feedback loops complete the picture last
draw(e7, 0.22); draw(e11, 0.22); draw(e18, 0.22); draw(e23, 0.35);
}
}
wait(0.5);
// Round 1 — two PRs race the pipeline in parallel. They ride together until the
// smoke test, where A ships and B fails and rolls back around to the start.
step("round-1") {
par {
seq {
show(a1.parts, 0.2);
route(a1, e1, 0.22, smooth); route(a1, e2, 0.22, smooth); route(a1, e3, 0.22, smooth);
route(a1, e4, 0.22, smooth); route(a1, e5, 0.22, smooth); route(a1, e8, 0.22, smooth);
route(a1, e9, 0.22, smooth); route(a1, e12, 0.22, smooth); route(a1, e13, 0.22, smooth);
route(a1, e15, 0.22, smooth); route(a1, e16, 0.22, smooth); route(a1, e19, 0.22, smooth);
route(a1, e20, 0.22, smooth); route(a1, e21, 0.22, smooth); route(a1, e24, 0.22, smooth);
route(a1, e26, 0.22, smooth); route(a1, e27, 0.22, smooth); route(a1, e29, 0.3, smooth);
}
seq {
show(b1.parts, 0.2);
route(b1, e1, 0.22, smooth); route(b1, e2, 0.22, smooth); route(b1, e3, 0.22, smooth);
route(b1, e4, 0.22, smooth); route(b1, e5, 0.22, smooth); route(b1, e8, 0.22, smooth);
route(b1, e9, 0.22, smooth); route(b1, e12, 0.22, smooth); route(b1, e13, 0.22, smooth);
route(b1, e15, 0.22, smooth); route(b1, e16, 0.22, smooth); route(b1, e19, 0.22, smooth);
route(b1, e20, 0.22, smooth);
route(b1, e22, 0.35, smooth); // smoke ok? no → rollback
route(b1, e23, 0.8, smooth); // rollback → start (around the perimeter)
}
}
}
wait(0.6);
step("clear-1") {
par { fade(a1.parts, 0.3); fade(b1.parts, 0.3); to(cd.hotpaths, trace, 0, 0.4); }
}
wait(0.3);
// Round 2 — another pair. They split at the unit-test gate: C carries on but is
// held at approval, while D fails its tests and loops back to rebuild.
step("round-2") {
par {
seq {
show(a2.parts, 0.2);
route(a2, e1, 0.22, smooth); route(a2, e2, 0.22, smooth); route(a2, e3, 0.22, smooth);
route(a2, e4, 0.22, smooth); route(a2, e5, 0.22, smooth); route(a2, e8, 0.22, smooth);
route(a2, e9, 0.22, smooth); route(a2, e12, 0.22, smooth); route(a2, e13, 0.22, smooth);
route(a2, e15, 0.22, smooth); route(a2, e16, 0.22, smooth); route(a2, e19, 0.22, smooth);
route(a2, e20, 0.22, smooth); route(a2, e21, 0.25, smooth);
route(a2, e25, 0.35, smooth); // approve prod? no → held
}
seq {
show(b2.parts, 0.2);
route(b2, e1, 0.22, smooth); route(b2, e2, 0.22, smooth); route(b2, e3, 0.22, smooth);
route(b2, e4, 0.22, smooth); route(b2, e5, 0.22, smooth); route(b2, e8, 0.22, smooth);
route(b2, e10, 0.35, smooth); // tests pass? no → debug
route(b2, e11, 0.6, smooth); // debug → build (loop back)
}
}
}
wait(0.6);
step("clear-2") {
par { fade(a2.parts, 0.3); fade(b2.parts, 0.3); to(cd.hotpaths, trace, 0, 0.4); }
}
wait(1.0);
c4-internet-banking
C4 Level 1 — System Context: the Internet Banking System in its world. A customer (a person, drawn as a box with a head), the system itself, and two external systems (e-mail, mainframe), joined by labelled relationships. Outline styling, [Type] tags and people-top tiers, all auto-laid with no coordinates.
title("Internet Banking — C4 System Context");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic");
text(kicker, (cx, h*0.055), "DIAGRAMS · C4 · SYSTEM CONTEXT");
text(headline, (cx, h*0.12), "Internet Banking — the big picture");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 24*u); bold(headline); wrap(headline, w*0.86);
// `c4(id, level)` — inside it, node kinds are C4 elements. The extra args are the
// box's description and (for containers) technology. No coordinates.
c4(bank, context);
node(cust, bank, "person", "Personal Banking Customer", "A customer of the bank with personal accounts");
node(ibs, bank, "system", "Internet Banking System", "Lets customers view accounts and make payments");
node(mail, bank, "external", "E-mail System", "Sends notifications to customers");
node(core, bank, "external", "Mainframe Banking System", "Stores core banking information");
connect(r1, cust, ibs); annotate(r1, "Views accounts, makes payments");
connect(r2, ibs, core); annotate(r2, "Reads/writes [XML/HTTPS]");
connect(r3, ibs, mail); annotate(r3, "Sends e-mail [SMTP]");
color(r1, cyan); color(r1.hot, cyan);
color(r2, cyan); color(r2.hot, cyan);
color(r3, gold); color(r3.hot, gold);
c4-internet-banking-containers
C4 Level 2 — Containers: zoom inside the system to a single-page app, an API application and a database, each carrying its technology in a [Container: tech] tag; the mainframe stays external. Same c4 container, one level down.
title("Internet Banking — C4 Containers");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic");
text(kicker, (cx, h*0.055), "DIAGRAMS · C4 · CONTAINER");
text(headline, (cx, h*0.12), "Inside the Internet Banking System");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 24*u); bold(headline); wrap(headline, w*0.86);
// Level 2 — zoom into the system: its containers, each with a technology. The
// technology is the optional 6th `node` argument, shown in the `[Container: …]` tag.
c4(bank, container);
node(cust, bank, "person", "Customer", "A retail banking customer");
node(spa, bank, "container", "Single-Page App", "Account summary and payments in the browser", "JavaScript / Angular");
node(api, bank, "container", "API Application", "Banking functionality via a JSON/HTTPS API", "Java / Spring Boot");
node(db, bank, "container", "Database", "Stores users, accounts and payments", "Oracle");
node(core, bank, "external", "Mainframe Banking System", "Stores core banking information");
connect(r1, cust, spa); annotate(r1, "Uses [HTTPS]");
connect(r2, spa, api); annotate(r2, "Calls [JSON/HTTPS]");
connect(r3, api, db); annotate(r3, "Reads/writes [JDBC]");
connect(r4, api, core); annotate(r4, "Uses [XML/HTTPS]");
color(r1, cyan); color(r1.hot, cyan);
color(r2, cyan); color(r2.hot, cyan);
color(r3, cyan); color(r3.hot, cyan);
color(r4, gold); color(r4.hot, gold);
c4-internet-banking-components
C4 Level 3 — Components: inside the API application, sign-in and accounts controllers, a security component and a mainframe facade — declared so related pairs sit adjacent, so the Uses and Reads/writes edges never cross an intervening box.
title("Internet Banking — C4 Components");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic");
text(kicker, (cx, h*0.055), "DIAGRAMS · C4 · COMPONENT");
text(headline, (cx, h*0.12), "Inside the API Application");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 24*u); bold(headline); wrap(headline, w*0.86);
// Level 3 — zoom into a container: its components. `component` is a C4 node kind;
// the dependencies it talks to sit below as externals.
// Declaration order = left-to-right in the tier, so keep each related pair
// adjacent (sign-in beside security, accounts beside the facade) — their "Uses"
// edge then sits in the gap instead of crossing an intervening box.
c4(api, component);
node(signin, api, "component", "Sign-In Controller", "Lets users sign in", "Spring MVC");
node(security, api, "component", "Security Component", "Authentication and access control", "Spring Bean");
node(accounts, api, "component", "Accounts Controller", "Provides account summaries", "Spring MVC");
node(facade, api, "component", "Mainframe Facade", "A facade over the mainframe", "Spring Bean");
node(db, api, "external", "Database", "Stores users, accounts and payments");
node(mf, api, "external", "Mainframe Banking System", "Stores core banking information");
connect(c1, signin, security); annotate(c1, "Uses");
connect(c2, accounts, facade); annotate(c2, "Uses");
connect(c3, security, db); annotate(c3, "Reads/writes [JDBC]");
connect(c4x, facade, mf); annotate(c4x, "Uses [XML/HTTPS]");
color(c1, cyan); color(c1.hot, cyan);
color(c2, cyan); color(c2.hot, cyan);
color(c3, cyan); color(c3.hot, cyan);
color(c4x, gold); color(c4x.hot, gold);
c4-zoom
The C4 differentiator — it moves. One diagram zooms from System Context into its Containers: zoom into the centred system, fade the surroundings, then reveal the containers — author-composed with zoom/fade/show and sticky chrome, no new vocabulary.
title("C4 — Zoom From Context Into Containers");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic"); sticky(mark);
text(kicker, (cx, h*0.055), "DIAGRAMS · C4 · CONTEXT → CONTAINER");
size(kicker, 15*u); color(kicker, dim); bold(kicker); sticky(kicker);
text(cap, (cx, h*0.93), "One system in its world.");
size(cap, 16*u); color(cap, dim); wrap(cap, w*0.86); sticky(cap);
// ---- Level 1: System Context (visible first) ----
c4(ctx, context);
node(cust, ctx, "person", "Customer", "A retail banking customer");
node(ibs, ctx, "system", "Internet Banking System", "View accounts, make payments");
node(mail, ctx, "external", "E-mail System", "Sends notifications");
node(core, ctx, "external", "Mainframe", "Core banking information");
connect(x1, cust, ibs); annotate(x1, "Uses");
connect(x2, ibs, mail); annotate(x2, "Sends e-mail [SMTP]");
connect(x3, ibs, core); annotate(x3, "Reads/writes [XML/HTTPS]");
color(x1, cyan); color(x1.hot, cyan);
color(x2, gold); color(x2.hot, gold);
color(x3, cyan); color(x3.hot, cyan);
// ---- Level 2: Containers (hidden until we zoom in) ----
c4(cnt, container);
node(pcust, cnt, "person", "Customer", "A retail banking customer");
node(spa, cnt, "container", "Single-Page App", "Account UI in the browser", "JavaScript / Angular");
node(api, cnt, "container", "API Application", "Banking functionality (JSON/HTTPS)", "Java / Spring Boot");
node(db, cnt, "container", "Database", "Users, accounts and payments", "Oracle");
node(mf, cnt, "external", "Mainframe", "Core banking information");
connect(y1, pcust, spa); annotate(y1, "Uses [HTTPS]");
connect(y2, spa, api); annotate(y2, "Calls [JSON/HTTPS]");
connect(y3, api, db); annotate(y3, "Reads/writes [JDBC]");
connect(y4, api, mf); annotate(y4, "Uses [XML/HTTPS]");
color(y1, cyan); color(y1.hot, cyan);
color(y2, cyan); color(y2.hot, cyan);
color(y3, cyan); color(y3.hot, cyan);
color(y4, gold); color(y4.hot, gold);
// The container diagram overlaps the context one (both centred) — hide it, and its
// duplicate frame, until the zoom reveals it.
hidden(cnt);
hidden(cnt.nodes); hidden(cnt.connections); hidden(cnt.hotpaths);
wait(0.8);
// Push the camera into the centred system, dropping everything around it.
step("zoom-in") {
par {
say(cap, "Zoom into the Internet Banking System…", 0.4);
zoom(2.6, 1.1, smooth);
fade(cust, 0.6); fade(mail, 0.6); fade(core, 0.6);
fade(ctx.connections, 0.6);
}
}
wait(0.2);
// The system dissolves and its containers rise in as the camera pulls back.
step("open") {
par {
say(cap, "…and its containers are inside: SPA → API → Database.", 0.4);
fade(ibs, 0.4);
zoom(1.0, 1.0, smooth);
show(cnt.nodes, 0.7);
show(cnt.connections, 0.8);
}
}
wait(1.4);
c4-story
One system, at every altitude — a full end-to-end walkthrough. Context → zoom in → the Containers build along a GET /accounts request as it travels browser→API→database → zoom into the API → the Components build along a sign-in call → zoom back out. The flow IS the reveal, led by a moving token.
title("C4 — One System, at Every Altitude");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic"); sticky(mark);
text(kicker, (cx, h*0.055), "DIAGRAMS · C4 · CONTEXT → CONTAINER → COMPONENT");
size(kicker, 15*u); color(kicker, dim); bold(kicker); sticky(kicker);
text(cap, (cx, h*0.93), "Every system lives in a world of people and neighbours.");
size(cap, 16*u); color(cap, dim); wrap(cap, w*0.86); sticky(cap);
// ---- Level 1: System Context ----
c4(ctx, context);
node(cust, ctx, "person", "Customer", "A retail banking customer");
node(sys, ctx, "system", "Internet Banking System", "View accounts, make payments");
node(mail, ctx, "external", "E-mail System", "Sends notifications");
node(core, ctx, "external", "Mainframe", "Core banking information");
connect(cx1, cust, sys); annotate(cx1, "Uses");
connect(cx2, sys, core); annotate(cx2, "Reads/writes [XML/HTTPS]");
connect(cx3, sys, mail); annotate(cx3, "Sends e-mail [SMTP]");
color(cx1, cyan); color(cx1.hot, cyan);
color(cx2, cyan); color(cx2.hot, cyan);
color(cx3, gold); color(cx3.hot, gold);
// ---- Level 2: Containers ----
c4(cnt, container);
node(ccust, cnt, "person", "Customer", "A retail banking customer");
node(spa, cnt, "container", "Single-Page App", "Account UI in the browser", "JavaScript / Angular");
node(api, cnt, "container", "API Application", "Banking functionality (JSON/HTTPS)", "Java / Spring Boot");
node(cdb, cnt, "container", "Database", "Users, accounts, payments", "Oracle");
node(cmf, cnt, "external", "Mainframe", "Core banking information");
connect(cn1, ccust, spa); annotate(cn1, "Uses [HTTPS]");
connect(cn2, spa, api); annotate(cn2, "Calls [JSON/HTTPS]");
connect(cn3, api, cdb); annotate(cn3, "Reads/writes [JDBC]");
connect(cn4, api, cmf); annotate(cn4, "Uses [XML/HTTPS]");
color(cn1, cyan); color(cn1.hot, cyan);
color(cn2, cyan); color(cn2.hot, cyan);
color(cn3, cyan); color(cn3.hot, cyan);
color(cn4, gold); color(cn4.hot, gold);
// ---- Level 3: Components (inside the API) ----
// Declaration order fills the 2x2 grid row-major, so ordering the pairs as
// (signin, accounts) then (security, facade) makes two clean vertical columns:
// sign-in → security → database | accounts → facade → mainframe
c4(cmp, component);
node(signin, cmp, "component", "Sign-In Controller", "Lets users sign in", "Spring MVC");
node(acct, cmp, "component", "Accounts Controller", "Account summaries", "Spring MVC");
node(sec, cmp, "component", "Security Component", "Authentication & access", "Spring Bean");
node(fac, cmp, "component", "Mainframe Facade", "Facade over the mainframe", "Spring Bean");
node(mdb, cmp, "external", "Database", "Users, accounts, payments");
node(mmf, cmp, "external", "Mainframe", "Core banking information");
connect(cp1, signin, sec); annotate(cp1, "Uses");
connect(cp2, acct, fac); annotate(cp2, "Uses");
connect(cp3, sec, mdb); annotate(cp3, "Reads/writes [JDBC]");
connect(cp4, fac, mmf); annotate(cp4, "Uses [XML/HTTPS]");
color(cp1, cyan); color(cp1.hot, cyan);
color(cp2, cyan); color(cp2.hot, cyan);
color(cp3, cyan); color(cp3.hot, cyan);
color(cp4, gold); color(cp4.hot, gold);
// tokens: one request per level
request(req, ccust, "GET /accounts");
request(req2, signin, "authorise");
// Start at Context; everything else is hidden and overlaps the same centre.
hidden(cnt); hidden(cnt.nodes); hidden(cnt.connections); hidden(cnt.hotpaths);
hidden(cmp); hidden(cmp.nodes); hidden(cmp.connections); hidden(cmp.hotpaths);
hidden(ctx.nodes);
hidden(req.parts); hidden(req2.parts);
// 1 — the system in its world (pieces arrive one by one)
step("context") {
seq {
stagger(0.12) { show(cust, 0.4); show(sys, 0.4); show(mail, 0.4); show(core, 0.4); }
draw(ctx.connections, 0.7);
}
}
wait(0.5);
// 2 — push into the system
step("into-system") {
par {
say(cap, "Zoom into the system…", 0.4);
zoom(2.5, 1.1, smooth);
fade(cust, 0.6); fade(mail, 0.6); fade(core, 0.6); fade(ctx.connections, 0.6);
}
}
wait(0.2);
// 3 — inside, the containers ASSEMBLE as the request travels through them.
// The dot leads: each box + its edge materialise just as the call arrives.
step("containers") {
seq {
par {
say(cap, "Inside — a request builds the containers as it travels.", 0.4);
fade(sys, 0.4);
zoom(1.0, 1.0, smooth);
}
par { show(ccust, 0.4); show(req.parts, 0.25); }
par { show(cn1, 0.6); show(cn1.text.bg, 0.6); show(cn1.text, 0.6); show(spa, 0.5); route(req, cn1, 0.6, smooth); }
par { show(cn2, 0.6); show(cn2.text.bg, 0.6); show(cn2.text, 0.6); show(api, 0.5); route(req, cn2, 0.6, smooth); }
par { show(cn3, 0.7); show(cn3.text.bg, 0.7); show(cn3.text, 0.7); show(cdb, 0.5); route(req, cn3, 0.7, smooth); }
par { show(cmf, 0.5); show(cn4, 0.6); show(cn4.text.bg, 0.6); show(cn4.text, 0.6); }
}
}
wait(0.5);
// 4 — push into the API container
step("into-api") {
par {
say(cap, "Zoom into the API…", 0.4);
zoom(2.4, 1.1, smooth);
fade(ccust, 0.6); fade(spa, 0.6); fade(cdb, 0.6); fade(cmf, 0.6);
fade(cnt.connections, 0.6); fade(req.parts, 0.4);
}
}
wait(0.2);
// 5 — inside the API, the components ASSEMBLE as the sign-in call travels.
step("components") {
seq {
par {
say(cap, "…inside, a sign-in call builds the components it touches.", 0.4);
fade(api, 0.4);
zoom(1.0, 1.0, smooth);
}
par { show(signin, 0.4); show(req2.parts, 0.25); }
par { show(cp1, 0.6); show(cp1.text.bg, 0.6); show(cp1.text, 0.6); show(sec, 0.5); route(req2, cp1, 0.6, smooth); }
par { show(cp3, 0.7); show(cp3.text.bg, 0.7); show(cp3.text, 0.7); show(mdb, 0.5); route(req2, cp3, 0.7, smooth); }
par {
show(acct, 0.5); show(fac, 0.5); show(mmf, 0.5);
show(cp2, 0.6); show(cp2.text.bg, 0.6); show(cp2.text, 0.6);
show(cp4, 0.6); show(cp4.text.bg, 0.6); show(cp4.text, 0.6);
}
}
}
wait(0.5);
// 8 — climb back to the containers (fade components out, pull back, reveal)
step("back-to-containers") {
seq {
par {
say(cap, "Zoom back out to the containers…", 0.4);
fade(cmp.nodes, 0.4); fade(cmp.connections, 0.4); fade(req2.parts, 0.3);
zoom(0.8, 0.5, smooth);
}
par {
zoom(1.0, 0.6, smooth);
show(ccust, 0.5); show(spa, 0.5); show(api, 0.5); show(cdb, 0.5); show(cmf, 0.5);
show(cnt.connections, 0.5);
}
}
}
wait(0.4);
// 9 — and out to the whole system in its world
step("back-to-context") {
seq {
par {
say(cap, "…and out to the whole system, at every altitude. That is manic.", 0.5);
fade(ccust, 0.4); fade(spa, 0.4); fade(api, 0.4); fade(cdb, 0.4); fade(cmf, 0.4);
fade(cnt.connections, 0.4);
zoom(0.8, 0.5, smooth);
}
par {
zoom(1.0, 0.6, smooth);
show(cust, 0.6); show(sys, 0.6); show(mail, 0.6); show(core, 0.6); show(ctx.connections, 0.6);
}
}
}
wait(1.6);
c4-test
The canonical bigbank Container diagram, translated straight from the Python diagrams library. Five containers auto-split into a balanced grid, and the long notification edge from the e-mail system back to the customer routes around the margin instead of bisecting the diagram — dense C4, still readable.
title("Container diagram — Internet Banking System");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.04), "Made With Manic");
text(kicker, (cx, h*0.055), "DIAGRAMS · C4 · CONTAINER");
text(headline, (cx, h*0.12), "Container diagram for Internet Banking System");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 22*u); bold(headline); wrap(headline, w*0.86);
// One c4 container diagram. Tiers do the grouping — person on top, the internal
// containers in the middle (they auto-split into balanced sub-rows), external
// systems on the bottom. Declaration order = left→right / row-major, so keep
// related boxes adjacent (web beside spa, spa's path down to api beside db).
c4(ib, container);
// person — top tier
node(cust, ib, "person", "Personal Banking Customer", "A customer of the bank, with personal bank accounts");
// internal containers — middle tier
node(web, ib, "container", "Web Application", "Delivers static content and the Internet banking SPA", "Java / Spring MVC");
node(spa, ib, "container", "Single-Page Application", "Internet banking in the web browser", "JavaScript / Angular");
node(mob, ib, "container", "Mobile App", "A subset of the banking functionality", "Xamarin");
node(api, ib, "container", "API Application", "Banking functionality via a JSON/HTTPS API", "Java / Spring MVC");
node(db, ib, "container", "Database", "Stores users, credentials and access logs", "Oracle");
// external systems — bottom tier
node(email, ib, "external", "E-mail System", "The internal Microsoft Exchange system");
node(mf, ib, "external", "Mainframe Banking System", "Core banking: customers, accounts, transactions");
connect(e1, cust, web); annotate(e1, "Visits bigbank.com/ib [HTTPS]");
connect(e2, cust, spa); annotate(e2, "Views balances, makes payments");
connect(e3, cust, mob); annotate(e3, "Views balances, makes payments");
connect(e4, web, spa); annotate(e4, "Delivers to the browser");
connect(e5, spa, api); annotate(e5, "API calls [JSON/HTTPS]");
connect(e6, mob, api); annotate(e6, "API calls [JSON/HTTPS]");
connect(e7, api, db); annotate(e7, "Reads/writes [JDBC]");
connect(e8, api, email); annotate(e8, "Sends e-mail [SMTP]");
connect(e9, api, mf); annotate(e9, "API calls [XML/HTTPS]");
connect(e10, email, cust); annotate(e10, "Sends e-mails to");
color(e1, cyan); color(e1.hot, cyan);
color(e2, cyan); color(e2.hot, cyan);
color(e3, cyan); color(e3.hot, cyan);
color(e4, cyan); color(e4.hot, cyan);
color(e5, cyan); color(e5.hot, cyan);
color(e6, cyan); color(e6.hot, cyan);
color(e7, cyan); color(e7.hot, cyan);
color(e8, gold); color(e8.hot, gold);
color(e9, gold); color(e9.hot, gold);
color(e10, gold); color(e10.hot, gold);
systems-rabbitmq-consumers
Authored one-of-many delivery: messages 101/102/103 explicitly select different workers — the kit never infers RabbitMQ or round-robin behaviour.
title("RabbitMQ Consumers — One Message, One Worker");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.14, h*0.035), "Made With Manic");
text(kicker, (cx, h*0.055), "SYSTEMS KIT · COMPETING CONSUMERS");
text(headline, (cx, h*0.125), "One queue. Three workers. One owner per message.");
text(caption, (cx, h*0.94), "Cold paths show every legal route; the moving job shows the route selected now.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 29*u); bold(headline); wrap(headline, w*0.88);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86);
if h > 1.25*w {
architecture(runtime, (cx, h*0.52), w*0.84, h*0.70);
}
else {
architecture(runtime, (cx, h*0.52), w*0.90, h*0.68);
}
node(queue, runtime, "queue", "RabbitMQ Queue");
cluster(consumers, runtime, "RABBITMQ CONSUMERS");
node(worker1, consumers, "service", "Pod Worker 1");
node(worker2, consumers, "service", "Pod Worker 2");
node(worker3, consumers, "service", "Pod Worker 3");
node(database, runtime, "database", "Database");
// Every lane is structural possibility. Explicit ports keep the upper and
// lower routes orthogonal without changing the semantic connection identity.
connect(queueToWorker1, queue, worker1, orthogonal, right, left);
connect(queueToWorker2, queue, worker2);
connect(queueToWorker3, queue, worker3, orthogonal, right, left);
connect(worker1ToDatabase, worker1, database, orthogonal, right, left);
connect(worker2ToDatabase, worker2, database);
connect(worker3ToDatabase, worker3, database, orthogonal, right, left);
message(job101, queue, "101");
message(job102, queue, "102");
message(job103, queue, "103");
hidden(kicker); hidden(headline); hidden(caption);
hidden(runtime.nodes); hidden(consumers.parts);
hidden(job101.parts); hidden(job102.parts); hidden(job103.parts);
untraced(runtime.connections);
step("queue-and-consumer-pool") {
par {
show(kicker, 0.30); show(headline, 0.45); show(caption, 0.40);
show(queue, 0.35);
show(consumers.parts, 0.45);
stagger(0.10) {
show(worker1, 0.32);
show(worker2, 0.32);
show(worker3, 0.32);
}
say(caption, "The queue and three workers are structure only. No delivery policy is inferred from their icons.", 0.40);
}
}
wait(0.25);
step("shared-database") {
par {
show(database, 0.38);
draw(runtime.connections, 1.10, smooth);
say(caption, "Each worker can consume from RabbitMQ and write its result to the same database.", 0.40);
}
}
wait(0.35);
step("message-101-selects-worker-2") {
par {
seq {
show(job101.parts, 0.18);
route(job101, queueToWorker2, 0.90, linear);
route(job101, worker2ToDatabase, 0.90, linear);
}
say(caption, "Message 101 is delivered to Worker 2 only. The upper and lower consumer routes stay cold.", 0.40);
}
}
wait(0.35);
step("message-102-selects-worker-1") {
par {
fade(job101.parts, 0.20);
fade(queueToWorker2.hot, 0.25); fade(worker2ToDatabase.hot, 0.25);
seq {
show(job102.parts, 0.18);
route(job102, queueToWorker1, 1.05, linear);
route(job102, worker1ToDatabase, 1.05, linear);
}
say(caption, "The next message can select Worker 1. Orthogonal turns preserve the same moving identity.", 0.40);
}
}
wait(0.35);
step("message-103-selects-worker-3") {
par {
fade(job102.parts, 0.20);
fade(queueToWorker1.hot, 0.25); fade(worker1ToDatabase.hot, 0.25);
seq {
show(job103.parts, 0.18);
route(job103, queueToWorker3, 1.05, linear);
route(job103, worker3ToDatabase, 1.05, linear);
}
say(caption, "Message 103 selects Worker 3: three jobs were shared, never broadcast as three copies.", 0.40);
}
}
wait(0.45);
step("takeaway") {
par {
pulse(queue, 0.65); pulse(consumers.parts, 0.75); pulse(database, 0.65);
say(caption, "Manic does not guess RabbitMQ semantics—the creator explicitly authors each selected delivery.", 0.45);
}
}
wait(1.30);
aws-three-tier-web-application
Presentation / Application / Data as three responsive tiers; one request travels Route 53 → CloudFront → ELB → ECS → ElastiCache → RDS.
title("Designing and Building a Three-Tier Web Application Architecture on AWS");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "AWS ARCHITECTURE · THREE TIERS");
text(headline, (cx, h*0.145), "Designing a Three-Tier Web Application");
text(caption, (cx, h*0.91), "Each tier has one responsibility—and the request keeps moving.");
size(kicker, 17*u); color(kicker, dim); bold(kicker);
size(headline, 32*u); bold(headline); wrap(headline, w*0.86);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.84);
if h > 1.25*w {
architecture(presentation, (cx, h*0.30), w*0.72, h*0.18);
architecture(application, (cx, h*0.52), w*0.72, h*0.18);
architecture(dataTier, (cx, h*0.74), w*0.72, h*0.18);
text(presentationLabel, (w*0.13, h*0.30), "1 · PRESENTATION");
text(applicationLabel, (w*0.13, h*0.52), "2 · APPLICATION");
text(dataLabel, (w*0.13, h*0.74), "3 · DATA");
}
else {
architecture(presentation, (w*0.20, h*0.50), w*0.27, h*0.55);
architecture(application, (w*0.50, h*0.50), w*0.27, h*0.55);
architecture(dataTier, (w*0.80, h*0.50), w*0.27, h*0.55);
text(presentationLabel, (w*0.20, h*0.235), "1 · PRESENTATION");
text(applicationLabel, (w*0.50, h*0.235), "2 · APPLICATION");
text(dataLabel, (w*0.80, h*0.235), "3 · DATA");
}
size(presentationLabel, 16*u); color(presentationLabel, cyan); bold(presentationLabel);
size(applicationLabel, 16*u); color(applicationLabel, magenta); bold(applicationLabel);
size(dataLabel, 16*u); color(dataLabel, gold); bold(dataLabel);
node(dns, presentation, "aws:route53", "Route 53");
node(cdn, presentation, "aws:cloudfront", "CloudFront");
node(balancer, application, "aws:elb", "Load Balancer");
node(app, application, "aws:ecs", "ECS Service");
node(cache, dataTier, "aws:elasticache", "ElastiCache");
node(database, dataTier, "aws:rds", "Amazon RDS");
connect(resolvePath, dns, cdn);
connect(toApplication, cdn, balancer);
connect(toService, balancer, app);
connect(toCache, app, cache);
connect(toDatabase, cache, database);
request(webRequest, dns, "GET /checkout");
hidden(presentation.nodes); hidden(application.nodes); hidden(dataTier.nodes);
untraced(resolvePath); untraced(toApplication); untraced(toService);
untraced(toCache); untraced(toDatabase);
hidden(webRequest.parts);
step("three-responsibilities") {
par {
show(presentation.nodes, 0.55);
say(caption, "Presentation receives users, resolves DNS, and serves content from the edge.", 0.40);
}
}
wait(0.20);
step("application-tier") {
par {
show(application.nodes, 0.55);
say(caption, "Application distributes traffic and runs stateless business logic.", 0.40);
}
}
wait(0.20);
step("data-tier") {
par {
show(dataTier.nodes, 0.55);
say(caption, "Data accelerates repeated reads and persists durable records.", 0.40);
}
}
wait(0.25);
step("connect-the-tiers") {
par {
stagger(0.10) {
draw(resolvePath, 0.35); draw(toApplication, 0.35); draw(toService, 0.35);
draw(toCache, 0.35); draw(toDatabase, 0.35);
}
show(webRequest.parts, 0.30);
say(caption, "Now follow one checkout request through the complete design.", 0.40);
}
}
wait(0.20);
step("presentation-flow") {
par {
route(webRequest, resolvePath, 0.80, smooth);
say(caption, "Route 53 resolves the domain; CloudFront brings the experience closer to users.", 0.40);
}
}
step("load-balance") {
par {
route(webRequest, toApplication, 0.95, smooth);
say(caption, "The load balancer is the controlled entry into the application tier.", 0.40);
}
}
step("run-the-application") {
par {
route(webRequest, toService, 0.80, smooth);
say(caption, "ECS runs replicated application tasks without mixing them with stored data.", 0.40);
}
}
step("read-fast") {
par {
route(webRequest, toCache, 0.95, smooth);
say(caption, "ElastiCache answers hot reads quickly and protects the database from repeated work.", 0.40);
}
}
step("persist-safely") {
par {
route(webRequest, toDatabase, 0.80, smooth);
say(caption, "Amazon RDS remains the durable source of truth for the transaction.", 0.40);
}
}
wait(0.30);
step("takeaway") {
par {
pulse(presentation.nodes, 0.70);
pulse(application.nodes, 0.70);
pulse(dataTier.nodes, 0.70);
say(caption, "Three tiers isolate delivery, computation, and state—so each can scale and evolve independently.", 0.45);
}
}
wait(1.40);
aws-event-processing-clusters-poc
Nested clusters + parallel topology: EKS → three ECS workers → SQS → three Lambda processors → S3 & Redshift, with one seeded hot path through the fan-out.
title("Event Processing on AWS — From Topology to Hot Path");
canvas("16:9");
template("mono");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.055), "Made With Manic");
text(kicker, (cx, h*0.055), "AWS ARCHITECTURE · EVENT PROCESSING");
text(headline, (cx, h*0.115), "One topology. One event. One visible hot path.");
text(caption, (cx, h*0.935), "Dashed relationships show every route the architecture allows.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 29*u); bold(headline); wrap(headline, w*0.88);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86);
if h > 1.25*w {
architecture(eventProcessing, (cx, h*0.51), w*0.82, h*0.70);
}
else {
architecture(eventProcessing, (cx, h*0.51), w*0.92, h*0.69);
}
node(source, eventProcessing, "aws:eks", "K8s Source");
cluster(flows, eventProcessing, "EVENT FLOWS");
cluster(workers, flows, "EVENT WORKERS");
node(worker1, workers, "aws:ecs", "Worker 1");
node(worker2, workers, "aws:ecs", "Worker 2");
node(worker3, workers, "aws:ecs", "Worker 3");
node(queue, flows, "aws:sqs", "Event Queue");
cluster(processing, flows, "PROCESSING");
node(proc1, processing, "aws:lambda", "Processor 1");
node(proc2, processing, "aws:lambda", "Processor 2");
node(proc3, processing, "aws:lambda", "Processor 3");
node(store, eventProcessing, "aws:s3", "Events Store");
node(warehouse, eventProcessing, "aws:redshift", "Analytics");
connect(toWorkers, source, workers);
connect(toQueue, workers, queue);
connect(toHandlers, queue, processing);
connect(toStore, processing, store);
connect(toAnalytics, processing, warehouse);
message(event, source, "EVENT");
hidden(eventProcessing.nodes);
hidden(flows.parts); hidden(workers.parts); hidden(processing.parts);
hidden(event.parts);
untraced(toWorkers); untraced(toQueue); untraced(toHandlers);
untraced(toStore); untraced(toAnalytics);
step("source") {
par {
show(source, 0.45);
say(caption, "EKS is the source; the rest of the system is still only structure.", 0.40);
}
}
wait(0.25);
step("ownership") {
par {
show(flows.parts, 0.50);
show(workers.parts, 0.45);
stagger(0.10) { show(worker1, 0.35); show(worker2, 0.35); show(worker3, 0.35); }
show(queue, 0.40);
show(processing.parts, 0.45);
stagger(0.10) { show(proc1, 0.35); show(proc2, 0.35); show(proc3, 0.35); }
show(store, 0.40); show(warehouse, 0.40);
say(caption, "Nested boundaries reveal ownership without hiding the individual services.", 0.40);
}
}
wait(0.20);
step("possible-topology") {
par {
draw(toWorkers, 0.55);
draw(toQueue, 0.55);
draw(toHandlers, 0.55);
draw(toStore, 0.55); draw(toAnalytics, 0.55);
say(caption, "Every dashed edge is a possible relationship—not a claim that data is flowing.", 0.40);
}
}
wait(0.35);
step("runtime-hot-path") {
par {
show(event.parts, 0.25);
hotpath(event, 6.20, 27);
say(caption, "The event now chooses valid branches and travels end-to-end as one continuous hot path.", 0.40);
}
}
wait(0.35);
step("takeaway") {
par {
pulse(event.parts, 0.70);
say(caption, "Cold topology explains relationships. The moving dot proves what happened now.", 0.45);
}
}
wait(1.40);
aws-clustered-web-services
Route 53 → ELB → ECS pool with an RDS primary/replica cluster and ElastiCache; request and response follow separately coloured lanes.
title("Clustered Web Services on AWS");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.025), "Made With Manic");
text(kicker, (cx, h*0.055), "AWS ARCHITECTURE · CLUSTERED WEB SERVICES");
text(headline, (cx, h*0.115), "Requests in. Responses back.");
text(caption, (cx, h*0.935), "Dashed paths describe what is connected—not what is moving now.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 29*u); bold(headline); wrap(headline, w*0.88);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86);
if h > 1.25*w {
architecture(clusteredWeb, (cx, h*0.51), w*0.84, h*0.70);
text(requestKey, (w*0.36, h*0.225), "REQUEST");
arrow(requestSample, (w*0.48, h*0.225), (w*0.64, h*0.225));
text(responseKey, (w*0.36, h*0.265), "RESPONSE");
arrow(responseSample, (w*0.48, h*0.265), (w*0.64, h*0.265));
}
else {
architecture(clusteredWeb, (cx, h*0.51), w*0.94, h*0.69);
text(requestKey, (w*0.105, h*0.205), "REQUEST");
arrow(requestSample, (w*0.15, h*0.205), (w*0.205, h*0.205));
text(responseKey, (w*0.105, h*0.245), "RESPONSE");
arrow(responseSample, (w*0.15, h*0.245), (w*0.205, h*0.245));
}
size(requestKey, 15*u); color(requestKey, fg); bold(requestKey);
size(responseKey, 15*u); hue(responseKey, 328, 0.90, 0.62); bold(responseKey);
color(requestSample, fg); stroke(requestSample, 3*u);
hue(responseSample, 328, 0.90, 0.62); dashed(responseSample, 10*u, 7*u);
stroke(responseSample, 4*u); glow(responseSample, 0.85);
hidden(requestKey); hidden(requestSample);
hidden(responseKey); hidden(responseSample);
node(dns, clusteredWeb, "aws:route53", "dns");
node(lb, clusteredWeb, "aws:elb", "lb");
cluster(services, clusteredWeb, "SERVICES");
node(web1, services, "aws:ecs", "web1");
node(web2, services, "aws:ecs", "web2");
node(web3, services, "aws:ecs", "web3");
cluster(database, clusteredWeb, "DB CLUSTER");
node(dbPrimary, database, "aws:rds", "userdb");
node(dbReplica, database, "aws:rds", "userdb ro");
node(memcached, clusteredWeb, "aws:elasticache", "memcached");
connect(dnsToLb, dns, lb);
connect(lbToServices, lb, services);
connect(servicesToDb, services, dbPrimary);
connect(servicesToCache, services, memcached, 145*u);
connect(dbToServices, dbPrimary, services, 42*u);
connect(servicesToLb, services, lb, 42*u);
connect(cacheToWeb2, memcached, web2, 145*u);
hue(dbToServices.hot, 328, 0.90, 0.62);
hue(servicesToLb.hot, 328, 0.90, 0.62);
hue(cacheToWeb2.hot, 328, 0.90, 0.62);
dashed(dbToServices.hot, 10*u, 7*u);
dashed(servicesToLb.hot, 10*u, 7*u);
dashed(cacheToWeb2.hot, 10*u, 7*u);
stroke(dbToServices.hot, 4.5*u); glow(dbToServices.hot, 0.95);
stroke(servicesToLb.hot, 4.5*u); glow(servicesToLb.hot, 0.95);
stroke(cacheToWeb2.hot, 4.5*u); glow(cacheToWeb2.hot, 0.95);
link(replication, dbPrimary.card, dbReplica.card);
dashed(replication); color(replication, dim); stroke(replication, 2.5*u);
message(pageRequest, dns, "GET");
message(cacheRead, web2, "READ");
hidden(clusteredWeb.nodes);
hidden(services.parts); hidden(database.parts);
hidden(pageRequest.parts); hidden(cacheRead.parts);
untraced(dnsToLb); untraced(lbToServices);
untraced(servicesToDb); untraced(servicesToCache);
untraced(dbToServices); untraced(servicesToLb); untraced(cacheToWeb2);
untraced(replication);
step("entry") {
par {
show(dns, 0.40);
show(lb, 0.40);
say(caption, "Route 53 and the load balancer form the public entry into the design.", 0.40);
}
}
wait(0.20);
step("service-pool") {
par {
show(services.parts, 0.45);
stagger(0.10) {
show(web1, 0.35);
show(web2, 0.35);
show(web3, 0.35);
}
say(caption, "Three ECS services share one ownership boundary; no runtime choice is implied yet.", 0.40);
}
}
wait(0.20);
step("state") {
par {
show(database.parts, 0.45);
show(dbPrimary, 0.40);
show(dbReplica, 0.40);
show(memcached, 0.40);
say(caption, "The database cluster and cache complete the static architecture.", 0.40);
}
}
wait(0.20);
step("possible-topology") {
par {
draw(dnsToLb, 0.40);
draw(lbToServices, 0.60);
draw(servicesToDb, 0.60);
draw(servicesToCache, 0.60);
draw(replication, 0.45);
show(requestKey, 0.35); show(requestSample, 0.35);
show(responseKey, 0.35); show(responseSample, 0.35);
say(caption, "Every dashed lane is possible. The next moving object will identify the path actually used.", 0.40);
}
}
wait(0.30);
step("request-to-database") {
par {
seq {
show(pageRequest.parts, 0.20);
route(pageRequest, dnsToLb, 0.65, smooth);
route(pageRequest, lbToServices, 0.90, smooth);
route(pageRequest, servicesToDb, 0.90, smooth);
}
say(caption, "This request follows one authored lane: dns → lb → web1 → userdb.", 0.40);
}
}
wait(0.35);
step("database-response") {
par {
seq {
par {
say(pageRequest.label, "200 OK", 0.25);
to(pageRequest.parts, hue, 328, 0.25, smooth);
}
route(pageRequest, dbToServices, 0.90, smooth);
route(pageRequest, servicesToLb, 0.90, smooth);
}
say(caption, "The same identity returns userdb → web1 → lb. DNS resolved the name; it is not the HTTP response path.", 0.40);
}
}
wait(0.35);
step("cache-round-trip") {
par {
seq {
show(cacheRead.parts, 0.20);
route(cacheRead, servicesToCache, 1.05, smooth);
par {
say(cacheRead.label, "HIT", 0.25);
to(cacheRead.parts, hue, 328, 0.25, smooth);
}
route(cacheRead, cacheToWeb2, 1.05, smooth);
}
say(caption, "A separate cache read travels web2 → memcached, becomes HIT, and returns to that same service.", 0.40);
}
}
wait(0.35);
step("replica-relationship") {
par {
flow(replication, 3.20, both, continuous);
say(caption, "The neutral database link can carry a two-way visual pulse only because the creator asked for it.", 0.40);
}
}
wait(0.35);
step("takeaway") {
par {
pulse(services.parts, 0.70);
pulse(database.parts, 0.70);
say(caption, "Architecture stays readable; objects, paths, timing, and composition tell the runtime story.", 0.45);
}
}
wait(1.40);
gcp-clustered-web-services
The same clustered-web story on Google Cloud: Cloud DNS → Load Balancing → GKE pool → Cloud SQL cluster + Memorystore — a pure provider swap.
title("Clustered Web Services on Google Cloud");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.025), "Made With Manic");
text(kicker, (cx, h*0.055), "GOOGLE CLOUD ARCHITECTURE · CLUSTERED WEB SERVICES");
text(headline, (cx, h*0.115), "Requests in. Responses back.");
text(caption, (cx, h*0.935), "Dashed paths describe what is connected—not what is moving now.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 29*u); bold(headline); wrap(headline, w*0.88);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86);
if h > 1.25*w {
architecture(clusteredWeb, (cx, h*0.51), w*0.84, h*0.70);
text(requestKey, (w*0.36, h*0.225), "REQUEST");
arrow(requestSample, (w*0.48, h*0.225), (w*0.64, h*0.225));
text(responseKey, (w*0.36, h*0.265), "RESPONSE");
arrow(responseSample, (w*0.48, h*0.265), (w*0.64, h*0.265));
}
else {
architecture(clusteredWeb, (cx, h*0.51), w*0.94, h*0.69);
text(requestKey, (w*0.105, h*0.205), "REQUEST");
arrow(requestSample, (w*0.15, h*0.205), (w*0.205, h*0.205));
text(responseKey, (w*0.105, h*0.245), "RESPONSE");
arrow(responseSample, (w*0.15, h*0.245), (w*0.205, h*0.245));
}
size(requestKey, 15*u); color(requestKey, fg); bold(requestKey);
size(responseKey, 15*u); hue(responseKey, 328, 0.90, 0.62); bold(responseKey);
color(requestSample, fg); stroke(requestSample, 3*u);
hue(responseSample, 328, 0.90, 0.62); dashed(responseSample, 10*u, 7*u);
stroke(responseSample, 4*u); glow(responseSample, 0.85);
hidden(requestKey); hidden(requestSample);
hidden(responseKey); hidden(responseSample);
node(dns, clusteredWeb, "gcp:dns", "cloud dns");
node(lb, clusteredWeb, "gcp:load-balancing", "load balancing");
cluster(services, clusteredWeb, "SERVICES");
node(web1, services, "gcp:kubernetes-engine", "web1");
node(web2, services, "gcp:kubernetes-engine", "web2");
node(web3, services, "gcp:kubernetes-engine", "web3");
cluster(database, clusteredWeb, "DB CLUSTER");
node(dbPrimary, database, "gcp:sql", "userdb");
node(dbReplica, database, "gcp:sql", "userdb ro");
node(cache, clusteredWeb, "gcp:memorystore", "memorystore");
connect(dnsToLb, dns, lb);
connect(lbToServices, lb, services);
connect(servicesToDb, services, dbPrimary);
connect(servicesToCache, services, cache, 145*u);
connect(dbToServices, dbPrimary, services, 42*u);
connect(servicesToLb, services, lb, 42*u);
connect(cacheToWeb2, cache, web2, 145*u);
hue(dbToServices.hot, 328, 0.90, 0.62);
hue(servicesToLb.hot, 328, 0.90, 0.62);
hue(cacheToWeb2.hot, 328, 0.90, 0.62);
dashed(dbToServices.hot, 10*u, 7*u);
dashed(servicesToLb.hot, 10*u, 7*u);
dashed(cacheToWeb2.hot, 10*u, 7*u);
stroke(dbToServices.hot, 4.5*u); glow(dbToServices.hot, 0.95);
stroke(servicesToLb.hot, 4.5*u); glow(servicesToLb.hot, 0.95);
stroke(cacheToWeb2.hot, 4.5*u); glow(cacheToWeb2.hot, 0.95);
link(replication, dbPrimary.card, dbReplica.card);
dashed(replication); color(replication, dim); stroke(replication, 2.5*u);
message(pageRequest, dns, "GET");
message(cacheRead, web2, "READ");
hidden(clusteredWeb.nodes);
hidden(services.parts); hidden(database.parts);
hidden(pageRequest.parts); hidden(cacheRead.parts);
untraced(dnsToLb); untraced(lbToServices);
untraced(servicesToDb); untraced(servicesToCache);
untraced(dbToServices); untraced(servicesToLb); untraced(cacheToWeb2);
untraced(replication);
step("entry") {
par {
show(dns, 0.40);
show(lb, 0.40);
say(caption, "Cloud DNS and Cloud Load Balancing form the public entry into the design.", 0.40);
}
}
wait(0.20);
step("service-pool") {
par {
show(services.parts, 0.45);
stagger(0.10) {
show(web1, 0.35);
show(web2, 0.35);
show(web3, 0.35);
}
say(caption, "Three GKE services share one ownership boundary; no runtime choice is implied yet.", 0.40);
}
}
wait(0.20);
step("state") {
par {
show(database.parts, 0.45);
show(dbPrimary, 0.40);
show(dbReplica, 0.40);
show(cache, 0.40);
say(caption, "The Cloud SQL cluster and Memorystore complete the static architecture.", 0.40);
}
}
wait(0.20);
step("possible-topology") {
par {
draw(dnsToLb, 0.40);
draw(lbToServices, 0.60);
draw(servicesToDb, 0.60);
draw(servicesToCache, 0.60);
draw(replication, 0.45);
show(requestKey, 0.35); show(requestSample, 0.35);
show(responseKey, 0.35); show(responseSample, 0.35);
say(caption, "Every dashed lane is possible. The next moving object will identify the path actually used.", 0.40);
}
}
wait(0.30);
step("request-to-database") {
par {
seq {
show(pageRequest.parts, 0.20);
route(pageRequest, dnsToLb, 0.65, smooth);
route(pageRequest, lbToServices, 0.90, smooth);
route(pageRequest, servicesToDb, 0.90, smooth);
}
say(caption, "This request follows one authored lane: dns → lb → web1 → userdb.", 0.40);
}
}
wait(0.35);
step("database-response") {
par {
seq {
par {
say(pageRequest.label, "200 OK", 0.25);
to(pageRequest.parts, hue, 328, 0.25, smooth);
}
route(pageRequest, dbToServices, 0.90, smooth);
route(pageRequest, servicesToLb, 0.90, smooth);
}
say(caption, "The same identity returns userdb → web1 → lb. Cloud DNS resolved the name; it is not the HTTP response path.", 0.40);
}
}
wait(0.35);
step("cache-round-trip") {
par {
seq {
show(cacheRead.parts, 0.20);
route(cacheRead, servicesToCache, 1.05, smooth);
par {
say(cacheRead.label, "HIT", 0.25);
to(cacheRead.parts, hue, 328, 0.25, smooth);
}
route(cacheRead, cacheToWeb2, 1.05, smooth);
}
say(caption, "A separate cache read travels web2 → memorystore, becomes HIT, and returns to that same service.", 0.40);
}
}
wait(0.35);
step("replica-relationship") {
par {
flow(replication, 3.20, both, continuous);
say(caption, "The neutral database link can carry a two-way visual pulse only because the creator asked for it.", 0.40);
}
}
wait(0.35);
step("takeaway") {
par {
pulse(services.parts, 0.70);
pulse(database.parts, 0.70);
say(caption, "Architecture stays readable; objects, paths, timing, and composition tell the runtime story.", 0.45);
}
}
wait(1.40);
gcp-message-collecting
GCP IoT: three IoT Core devices publish to Pub/Sub, and Dataflow fans out to a data lake, a processing branch, and a serverless branch — three-level nested clusters.
title("Message Collecting on Google Cloud — IoT to Data Lake");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.045), "Made With Manic");
text(kicker, (cx, h*0.05), "GOOGLE CLOUD · IOT MESSAGE COLLECTING");
text(headline, (cx, h*0.11), "Devices publish once. The pipeline fans out.");
text(caption, (cx, h*0.94), "Dashed relationships show every route the architecture allows.");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 26*u); bold(headline); wrap(headline, w*0.88);
size(caption, 17*u); color(caption, dim); wrap(caption, w*0.86);
architecture(collect, (cx, h*0.52), w*0.96, h*0.72);
cluster(sourceData, collect, "SOURCE OF DATA");
node(core1, sourceData, "gcp:iot-core", "core1");
node(core2, sourceData, "gcp:iot-core", "core2");
node(core3, sourceData, "gcp:iot-core", "core3");
node(pubsub, collect, "gcp:pubsub", "pubsub");
cluster(targets, collect, "TARGETS");
cluster(dataFlow, targets, "DATA FLOW");
node(df, dataFlow, "gcp:dataflow", "data flow");
cluster(dataLake, targets, "DATA LAKE");
node(bq, dataLake, "gcp:bigquery", "bq");
node(gcs, dataLake, "gcp:storage", "storage");
cluster(eventDriven, targets, "EVENT DRIVEN");
cluster(processing, eventDriven, "PROCESSING");
node(engine, processing, "gcp:app-engine", "engine");
node(bigtable, processing, "gcp:bigtable", "bigtable");
cluster(serverless, eventDriven, "SERVERLESS");
node(func, serverless, "gcp:functions", "func");
node(appengine, serverless, "gcp:app-engine", "appengine");
// --- topology ---
connect(toPubsub, sourceData, pubsub); // [core1,core2,core3] >> pubsub
connect(toFlow, pubsub, df); // pubsub >> flow
connect(toBq, df, bq); // flow >> bq
connect(toGcs, df, gcs); // flow >> storage
connect(toEngine, df, engine); // flow >> engine
connect(engineToBt, engine, bigtable); // engine >> bigtable
connect(toFunc, df, func); // flow >> func
connect(funcToApp, func, appengine); // func >> appengine
// --- colour every path by its RELATIONSHIP (cold dashed line + hot overlay) ---
color(toPubsub, cyan); color(toPubsub.hot, cyan);
color(toFlow, cyan); color(toFlow.hot, cyan);
color(toBq, gold); color(toBq.hot, gold);
color(toGcs, gold); color(toGcs.hot, gold);
color(toEngine, magenta); color(toEngine.hot, magenta);
color(engineToBt, magenta);color(engineToBt.hot, magenta);
color(toFunc, lime); color(toFunc.hot, lime);
color(funcToApp, lime); color(funcToApp.hot, lime);
// legend
text(legIn, (w*0.22, h*0.175), "INGEST"); size(legIn, 13); color(legIn, cyan); bold(legIn);
text(legLake, (w*0.36, h*0.175), "DATA LAKE"); size(legLake, 13); color(legLake, gold); bold(legLake);
text(legProc, (w*0.54, h*0.175), "PROCESSING"); size(legProc, 13); color(legProc, magenta); bold(legProc);
text(legSls, (w*0.72, h*0.175), "SERVERLESS"); size(legSls, 13); color(legSls, lime); bold(legSls);
hidden(legIn); hidden(legLake); hidden(legProc); hidden(legSls);
message(msg, pubsub, "msg");
hidden(collect.nodes);
hidden(sourceData.parts); hidden(targets.parts); hidden(dataFlow.parts);
hidden(dataLake.parts); hidden(eventDriven.parts); hidden(processing.parts); hidden(serverless.parts);
hidden(msg.parts);
untraced(toPubsub); untraced(toFlow); untraced(toBq); untraced(toGcs);
untraced(toEngine); untraced(engineToBt); untraced(toFunc); untraced(funcToApp);
step("devices") {
par {
show(sourceData.parts, 0.45);
stagger(0.10) { show(core1, 0.35); show(core2, 0.35); show(core3, 0.35); }
show(pubsub, 0.40);
say(caption, "Three IoT Core devices publish into one Pub/Sub topic.", 0.40);
}
}
wait(0.20);
step("targets") {
par {
show(targets.parts, 0.5);
show(dataFlow.parts, 0.4); show(df, 0.4);
show(dataLake.parts, 0.4); show(bq, 0.4); show(gcs, 0.4);
show(eventDriven.parts, 0.4);
show(processing.parts, 0.4); show(engine, 0.4); show(bigtable, 0.4);
show(serverless.parts, 0.4); show(func, 0.4); show(appengine, 0.4);
say(caption, "Dataflow is the hub; targets nest into a data lake, a processing branch, and a serverless branch.", 0.40);
}
}
wait(0.25);
step("possible-topology") {
par {
stagger(0.07) {
draw(toPubsub, 0.4); draw(toFlow, 0.4); draw(toBq, 0.4); draw(toGcs, 0.4);
draw(toEngine, 0.4); draw(engineToBt, 0.4); draw(toFunc, 0.4); draw(funcToApp, 0.4);
}
show(legIn, 0.3); show(legLake, 0.3); show(legProc, 0.3); show(legSls, 0.3);
say(caption, "Every dashed edge is a possible relationship—not a claim that data is flowing.", 0.40);
}
}
wait(0.30);
step("collect") {
par {
seq {
show(msg.parts, 0.2);
route(msg, toFlow, 0.9, smooth);
}
say(caption, "A published message reaches Dataflow through Pub/Sub.", 0.40);
}
}
wait(0.25);
step("fan-out") {
par {
flow(toBq, 1.0); flow(toGcs, 1.0);
seq { flow(toEngine, 0.9); flow(engineToBt, 0.9); }
seq { flow(toFunc, 0.9); flow(funcToApp, 0.9); }
say(caption, "Dataflow fans out: BigQuery + storage (lake), App Engine → Bigtable, and Functions → App Engine.", 0.40);
}
}
wait(0.30);
step("takeaway") {
par {
pulse(targets.parts, 0.70);
say(caption, "One publish; many destinations—each branch owns its own runtime and storage.", 0.45);
}
}
wait(1.40);
k8s-three-tier
A three-tier app on Kubernetes: Ingress → web Deployment → api Service/Deployment → Redis + a PostgreSQL StatefulSet, following one checkout request.
title("A Three-Tier Application on Kubernetes — Following One Request");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.06), "Made With Manic");
text(kicker, (cx, h*0.065), "KUBERNETES · THREE TIERS · ONE DATA FLOW");
text(headline, (cx, h*0.145), "A Three-Tier App on Kubernetes");
text(caption, (cx, h*0.91), "Ingress in, pods compute, state persists — and the request keeps moving.");
size(kicker, 17*u); color(kicker, dim); bold(kicker);
size(headline, 32*u); bold(headline); wrap(headline, w*0.86);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.84);
if h > 1.25*w {
architecture(presentation, (cx, h*0.30), w*0.72, h*0.18);
architecture(application, (cx, h*0.52), w*0.72, h*0.18);
architecture(dataTier, (cx, h*0.74), w*0.72, h*0.18);
text(presentationLabel, (w*0.13, h*0.30), "1 · PRESENTATION");
text(applicationLabel, (w*0.13, h*0.52), "2 · APPLICATION");
text(dataLabel, (w*0.13, h*0.74), "3 · DATA");
}
else {
architecture(presentation, (w*0.20, h*0.50), w*0.27, h*0.55);
architecture(application, (w*0.50, h*0.50), w*0.27, h*0.55);
architecture(dataTier, (w*0.80, h*0.50), w*0.27, h*0.55);
text(presentationLabel, (w*0.20, h*0.235), "1 · PRESENTATION");
text(applicationLabel, (w*0.50, h*0.235), "2 · APPLICATION");
text(dataLabel, (w*0.80, h*0.235), "3 · DATA");
}
size(presentationLabel, 16*u); color(presentationLabel, cyan); bold(presentationLabel);
size(applicationLabel, 16*u); color(applicationLabel, magenta); bold(applicationLabel);
size(dataLabel, 16*u); color(dataLabel, gold); bold(dataLabel);
node(ingress, presentation, "k8s:ing", "Ingress");
node(web, presentation, "k8s:deploy", "web Deployment x3");
node(apisvc, application, "k8s:svc", "api Service");
node(api, application, "k8s:deploy", "api Deployment x3");
node(cache, dataTier, "onprem:redis", "Redis");
node(database, dataTier, "onprem:postgresql", "PostgreSQL (StatefulSet)");
connect(toWeb, ingress, web);
connect(toApiSvc, web, apisvc);
connect(toApi, apisvc, api);
connect(toCache, api, cache);
connect(toDatabase, cache, database);
request(userRequest, ingress, "GET /checkout");
// --- colour every path by its RELATIONSHIP (cold dashed line + hot overlay) ---
color(toWeb, cyan); color(toWeb.hot, cyan);
color(toApiSvc, cyan); color(toApiSvc.hot, cyan);
color(toApi, cyan); color(toApi.hot, cyan);
color(toCache, magenta); color(toCache.hot, magenta);
color(toDatabase, gold); color(toDatabase.hot, gold);
// legend
text(legReq, (w*0.30, h*0.175), "REQUEST"); size(legReq, 13); color(legReq, cyan); bold(legReq);
text(legCache, (w*0.48, h*0.175), "CACHE"); size(legCache, 13); color(legCache, magenta); bold(legCache);
text(legDb, (w*0.62, h*0.175), "PERSIST"); size(legDb, 13); color(legDb, gold); bold(legDb);
hidden(legReq); hidden(legCache); hidden(legDb);
hidden(presentation.nodes); hidden(application.nodes); hidden(dataTier.nodes);
untraced(toWeb); untraced(toApiSvc); untraced(toApi);
untraced(toCache); untraced(toDatabase);
hidden(userRequest.parts);
step("presentation-tier") {
par {
show(presentation.nodes, 0.55);
say(caption, "Presentation: the Ingress terminates traffic and routes it to the web pods.", 0.40);
}
}
wait(0.20);
step("application-tier") {
par {
show(application.nodes, 0.55);
say(caption, "Application: a Service load-balances across stateless api pods that scale on demand.", 0.40);
}
}
wait(0.20);
step("data-tier") {
par {
show(dataTier.nodes, 0.55);
say(caption, "Data: Redis serves hot reads; a PostgreSQL StatefulSet holds durable state.", 0.40);
}
}
wait(0.25);
step("connect-the-tiers") {
par {
stagger(0.10) {
draw(toWeb, 0.35); draw(toApiSvc, 0.35); draw(toApi, 0.35);
draw(toCache, 0.35); draw(toDatabase, 0.35);
}
show(userRequest.parts, 0.30);
show(legReq, 0.3); show(legCache, 0.3); show(legDb, 0.3);
say(caption, "Now follow one checkout request through the whole cluster.", 0.40);
}
}
wait(0.20);
step("ingress-to-web") {
par {
route(userRequest, toWeb, 0.85, smooth);
say(caption, "The Ingress forwards the request to a web pod via the Service.", 0.40);
}
}
step("web-to-api") {
par {
route(userRequest, toApiSvc, 0.90, smooth);
say(caption, "The web tier calls the api Service — one controlled entry into business logic.", 0.40);
}
}
step("into-a-pod") {
par {
route(userRequest, toApi, 0.80, smooth);
say(caption, "The Service load-balances into one replicated api pod.", 0.40);
}
}
step("read-fast") {
par {
route(userRequest, toCache, 0.90, smooth);
say(caption, "Redis answers the hot read first, shielding the database from repeated work.", 0.40);
}
}
step("persist-safely") {
par {
route(userRequest, toDatabase, 0.85, smooth);
say(caption, "On a miss, the PostgreSQL StatefulSet is the durable source of truth.", 0.40);
}
}
wait(0.30);
step("takeaway") {
par {
pulse(presentation.nodes, 0.70);
pulse(application.nodes, 0.70);
pulse(dataTier.nodes, 0.70);
say(caption, "Ingress, Services, Deployments and a StatefulSet keep delivery, compute, and state independent.", 0.45);
}
}
wait(1.40);
k8s-stateful-architecture
A StatefulSet’s storage: a Service, three pods, their PVCs, and the PV/StorageClass that provision them — generated with a for loop, coloured by access/ownership/provisioning.
title("A Stateful Set on Kubernetes — Pods, Claims, and Volumes");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.045), "Made With Manic");
text(kicker, (cx, h*0.05), "KUBERNETES · STATEFUL ARCHITECTURE");
text(headline, (cx, h*0.11), "Every replica keeps its own storage.");
text(caption, (cx, h*0.94), "Dashed relationships show every route the architecture allows.");
size(kicker, 15*u); color(kicker, dim); bold(kicker);
size(headline, 26*u); bold(headline); wrap(headline, w*0.88);
size(caption, 17*u); color(caption, dim); wrap(caption, w*0.86);
architecture(stateful, (cx, h*0.52), w*0.94, h*0.72);
cluster(apps, stateful, "APPS");
node(svc, apps, "k8s:svc", "svc");
node(sts, apps, "k8s:sts", "sts");
cluster(pods, stateful, "PODS");
for i in 0..3 {
node(pod{i}, pods, "k8s:pod", "pod");
}
cluster(claims, stateful, "CLAIMS");
for i in 0..3 {
node(pvc{i}, claims, "k8s:pvc", "pvc");
}
cluster(storage, stateful, "STORAGE");
node(pv, storage, "k8s:pv", "pv");
node(sc, storage, "k8s:sc", "sc");
// topology: svc >> pod >> pvc, pod - sts - pvc, and sc >> pv >> pvc (provisioning)
for i in 0..3 {
connect(svcPod{i}, svc, pod{i});
connect(podPvc{i}, pod{i}, pvc{i});
connect(podSts{i}, pod{i}, sts);
connect(stsPvc{i}, sts, pvc{i});
connect(pvPvc{i}, pv, pvc{i});
}
connect(scPv, sc, pv);
// --- colour every path by its RELATIONSHIP (cold dashed line + hot overlay) ---
for i in 0..3 {
color(svcPod{i}, cyan); // ACCESS
color(podPvc{i}, cyan);
color(podSts{i}, magenta); // OWNERSHIP
color(stsPvc{i}, magenta);
color(pvPvc{i}, gold); // PROVISIONING
}
color(scPv, gold); color(scPv.hot, gold);
text(legAcc, (w*0.30, h*0.175), "ACCESS"); size(legAcc, 13); color(legAcc, cyan); bold(legAcc);
text(legOwn, (w*0.46, h*0.175), "OWNERSHIP"); size(legOwn, 13); color(legOwn, magenta); bold(legOwn);
text(legProv, (w*0.64, h*0.175), "PROVISIONING"); size(legProv, 13); color(legProv, gold); bold(legProv);
hidden(legAcc); hidden(legOwn); hidden(legProv);
hidden(stateful.nodes);
hidden(apps.parts); hidden(pods.parts); hidden(claims.parts); hidden(storage.parts);
for i in 0..3 {
untraced(svcPod{i}); untraced(podPvc{i}); untraced(podSts{i});
untraced(stsPvc{i}); untraced(pvPvc{i});
}
untraced(scPv);
step("apps") {
par {
show(apps.parts, 0.5);
show(svc, 0.4); show(sts, 0.4);
show(pods.parts, 0.4);
stagger(0.08) { show(pod0, 0.35); show(pod1, 0.35); show(pod2, 0.35); }
show(claims.parts, 0.4);
stagger(0.08) { show(pvc0, 0.35); show(pvc1, 0.35); show(pvc2, 0.35); }
say(caption, "A Service fronts three pods; a StatefulSet gives each pod a stable identity and claim.", 0.40);
}
}
wait(0.20);
step("storage") {
par {
show(storage.parts, 0.45);
show(pv, 0.4); show(sc, 0.4);
say(caption, "A StorageClass provisions PersistentVolumes that back each PersistentVolumeClaim.", 0.40);
}
}
wait(0.25);
step("possible-topology") {
par {
stagger(0.05) {
draw(svcPod0, 0.35); draw(svcPod1, 0.35); draw(svcPod2, 0.35);
draw(podPvc0, 0.35); draw(podPvc1, 0.35); draw(podPvc2, 0.35);
draw(podSts0, 0.35); draw(podSts1, 0.35); draw(podSts2, 0.35);
draw(stsPvc0, 0.35); draw(stsPvc1, 0.35); draw(stsPvc2, 0.35);
draw(scPv, 0.35);
draw(pvPvc0, 0.35); draw(pvPvc1, 0.35); draw(pvPvc2, 0.35);
}
show(legAcc, 0.3); show(legOwn, 0.3); show(legProv, 0.3);
say(caption, "Every dashed edge is a relationship: routing, ownership, and storage binding together.", 0.40);
}
}
wait(0.30);
step("app-access") {
par {
stagger(0.12) {
seq { flow(svcPod0, 0.7); flow(podPvc0, 0.7); }
seq { flow(svcPod1, 0.7); flow(podPvc1, 0.7); }
seq { flow(svcPod2, 0.7); flow(podPvc2, 0.7); }
}
say(caption, "Traffic: the Service reaches a pod, which reads and writes its own claim.", 0.40);
}
}
wait(0.25);
step("provisioning") {
par {
seq {
flow(scPv, 0.8);
par { flow(pvPvc0, 0.8); flow(pvPvc1, 0.8); flow(pvPvc2, 0.8); }
}
say(caption, "Provisioning: the StorageClass creates PVs that bind to each claim.", 0.40);
}
}
wait(0.30);
step("takeaway") {
par {
pulse(apps.parts, 0.7);
pulse(storage.parts, 0.7);
say(caption, "Stateful means identity plus durable storage—one claim and volume per replica.", 0.45);
}
}
wait(1.40);
k8s-cluster-components
The canonical Kubernetes components diagram — control plane (api hub, c-m, c-c-m, etcd, sched) + nodes + the cloud provider — drawn AND flowed with plain primitives, no systems kit at all.
title("Kubernetes Cluster Components");
canvas("16:9");
template("neon");
// ---- boxes (drawn first, behind; hollow-ish so icons read on top) ----
rect(clusterBox, (800, 380), 880, 560); outline(clusterBox, dim); opacity(clusterBox, 0.22);
rect(cpBox, (575, 440), 400, 430); outline(cpBox, cyan); opacity(cpBox, 0.28);
rect(node1Box, (1000, 320), 400, 150); outline(node1Box, dim); opacity(node1Box, 0.35);
rect(node2Box, (1000, 520), 400, 150); outline(node2Box, dim); opacity(node2Box, 0.35);
text(clusterLabel, (470, 130), "Kubernetes cluster"); size(clusterLabel, 26); bold(clusterLabel);
text(cpLabel, (490, 250), "Control Plane"); size(cpLabel, 20); color(cpLabel, cyan); bold(cpLabel);
text(n1Label, (855, 268), "Node"); size(n1Label, 18); color(n1Label, dim); bold(n1Label);
text(n2Label, (855, 468), "Node"); size(n2Label, 18); color(n2Label, dim); bold(n2Label);
// ---- cloud provider API (outside the cluster, left) ----
ellipse(cloud, (165, 360), 210, 120); outline(cloud, blue); opacity(cloud, 0.30);
text(cloudLabel, (165, 360), "Cloud provider\nAPI"); size(cloudLabel, 20); color(cloudLabel, blue); bold(cloudLabel);
// ---- control-plane connections (api is the hub) ----
line(lCm, (615, 400), (615, 320)); stroke(lCm, 3);
line(lSched,(615, 485), (615, 560)); stroke(lSched, 3);
line(lEtcd, (590, 465), (505, 500)); stroke(lEtcd, 3);
line(lCcmApi, (505, 375), (580, 425)); stroke(lCcmApi, 3); dashed(lCcmApi, 9, 7);
arrow(aCloud, (440, 360), (270, 360)); stroke(aCloud, 3);
line(lN1, (800, 320), (660, 425)); stroke(lN1, 3);
line(lN2, (800, 520), (660, 450)); stroke(lN2, 3);
// ---- component icons (placed at the reference positions) ----
image(api, (615, 440), "asset:diagrams/k8s/controlplane/api.png", 62, 62);
image(cm, (615, 285), "asset:diagrams/k8s/controlplane/c-m.png", 58, 58);
image(sched, (615, 590), "asset:diagrams/k8s/controlplane/sched.png", 58, 58);
image(ccm, (470, 360), "asset:diagrams/k8s/controlplane/c-c-m.png", 58, 58);
image(etcd, (470, 510), "asset:diagrams/k8s/infra/etcd.png", 58, 58);
image(kubelet1, (890, 335), "asset:diagrams/k8s/controlplane/kubelet.png", 54, 54);
image(kproxy1, (1100, 335), "asset:diagrams/k8s/controlplane/k-proxy.png", 54, 54);
image(kubelet2, (890, 535), "asset:diagrams/k8s/controlplane/kubelet.png", 54, 54);
image(kproxy2, (1100, 535), "asset:diagrams/k8s/controlplane/k-proxy.png", 54, 54);
// ---- icon captions ----
text(tApi, (615, 478), "api"); size(tApi, 15);
text(tCm, (615, 320), "c-m"); size(tCm, 15);
text(tSched, (615, 625), "sched"); size(tSched, 15);
text(tCcm, (470, 395), "c-c-m"); size(tCcm, 15);
text(tEtcd, (470, 545), "etcd"); size(tEtcd, 15);
text(tKl1, (890, 370), "kubelet"); size(tKl1, 14);
text(tKp1, (1100, 370), "k-proxy"); size(tKp1, 14);
text(tKl2, (890, 570), "kubelet"); size(tKl2, 14);
text(tKp2, (1100, 570), "k-proxy"); size(tKp2, 14);
// ---- data flow: ordinary manic verbs on the hand-drawn wires (NO systems kit) ----
text(cap, (cx, h*0.95), "Every component talks through the API server."); size(cap, 20); color(cap, dim); wrap(cap, w*0.8);
dot(packet, (800, 320), 11); color(packet, lime); hidden(packet);
wait(0.5);
par {
flow(lN1, 1.0); flow(lN2, 1.0);
say(cap, "kubelets report node and pod status up to the API server", 0.4);
}
par {
flow(lCm, 1.0); flow(lSched, 1.0);
say(cap, "the scheduler and controllers act only through the API server", 0.4);
}
par {
flow(lEtcd, 1.0);
say(cap, "the API server reads and writes all cluster state in etcd", 0.4);
}
par {
flow(aCloud, 1.0);
say(cap, "the cloud-controller-manager provisions infrastructure via the cloud API", 0.4);
}
wait(0.2);
par {
show(packet, 0.2);
say(cap, "one node update: kubelet -> API server -> etcd", 0.4);
}
seq {
travel(packet, lN1, 0.9, smooth);
travel(packet, lEtcd, 0.9, smooth);
}
pulse(api, 0.7);
wait(1.4);
serverless-processing
A mixed on-prem + AWS pipeline: Kafka + Docker engines → SQS (+ dead-letter) → Lambda → S3/Redshift, with a Fluentd→Kafka→Spark tap; paths coloured by relationship.
title("Serverless Event Processing — From Kafka to Cold Storage");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.15, h*0.055), "Made With Manic");
text(kicker, (cx, h*0.055), "SERVERLESS · EVENT PROCESSING · MIXED STACK");
text(headline, (cx, h*0.115), "Events in. Results, dead letters, and cold storage out.");
text(caption, (cx, h*0.935), "Dashed relationships show every route the architecture allows.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 27*u); bold(headline); wrap(headline, w*0.88);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86);
if h > 1.25*w {
architecture(pipeline, (cx, h*0.52), w*0.86, h*0.72);
}
else {
architecture(pipeline, (cx, h*0.52), w*0.94, h*0.70);
}
// --- the ingress + event source + a bold analytics tap (on-prem stack) ---
cluster(ingest, pipeline, "INGEST");
node(ingress, ingest, "onprem:fluentd", "logging");
node(source, ingest, "onprem:kafka", "Event-Sourcing");
node(spark, ingest, "onprem:spark", "Spark");
cluster(flows, pipeline, "EVENT FLOWS");
cluster(engines, flows, "DATA PROCESSING ENGINES");
node(clicks, engines, "onprem:docker", "click stream");
node(logins, engines, "onprem:docker", "user logins");
node(analytics, engines, "onprem:docker", "data analytics");
node(queue, flows, "aws:sqs", "results queue");
node(dlq, flows, "aws:sqs", "Dead Letter Queue");
cluster(processing, flows, "PROCESSING");
node(persist, processing, "aws:lambda", "persist results");
cluster(storage, flows, "STORAGE");
node(store, storage, "aws:s3", "cold storage");
node(dw, storage, "aws:redshift", "operational storage");
// --- topology (Mingrammer edges) ---
connect(parse, ingress, source); // ingress >> Edge("parse") >> source
connect(toSpark, source, spark); // source >> Edge(bold) >> Spark
connect(toEngines, source, engines); // source >> workers
connect(toQueue, engines, queue); // workers >> queue
connect(toHandlers, queue, processing); // queue >> handlers
connect(toDlq, queue, dlq, 120*u); // queue >> dlq
connect(toStore, processing, store); // handlers >> store
connect(toDw, processing, dw); // handlers >> dw
// --- colour every path by its RELATIONSHIP (cold dashed line + hot overlay) ---
color(parse, cyan); color(parse.hot, cyan);
color(toEngines, cyan); color(toEngines.hot, cyan);
color(toQueue, cyan); color(toQueue.hot, cyan);
color(toHandlers, cyan); color(toHandlers.hot, cyan);
color(toSpark, magenta); color(toSpark.hot, magenta);
color(toStore, gold); color(toStore.hot, gold);
color(toDw, gold); color(toDw.hot, gold);
color(toDlq, red); color(toDlq.hot, red);
// legend
text(legPipe, (w*0.24, h*0.175), "PIPELINE"); size(legPipe, 13); color(legPipe, cyan); bold(legPipe);
text(legAna, (w*0.40, h*0.175), "ANALYTICS"); size(legAna, 13); color(legAna, magenta); bold(legAna);
text(legStore, (w*0.58, h*0.175), "STORAGE"); size(legStore, 13); color(legStore, gold); bold(legStore);
text(legDlq, (w*0.74, h*0.175), "DEAD-LETTER"); size(legDlq, 13); color(legDlq, red); bold(legDlq);
hidden(legPipe); hidden(legAna); hidden(legStore); hidden(legDlq);
message(event, source, "event");
hidden(pipeline.nodes);
hidden(ingest.parts); hidden(flows.parts); hidden(engines.parts); hidden(processing.parts); hidden(storage.parts);
hidden(event.parts);
untraced(parse); untraced(toSpark); untraced(toEngines); untraced(toQueue);
untraced(toHandlers); untraced(toDlq); untraced(toStore); untraced(toDw);
step("ingest") {
par {
show(ingest.parts, 0.45);
show(ingress, 0.40);
show(source, 0.40);
show(spark, 0.40);
say(caption, "Fluentd parses raw logs into Kafka; a bold tap also streams the source into Spark.", 0.40);
}
}
wait(0.20);
step("event-flows") {
par {
show(flows.parts, 0.50);
show(engines.parts, 0.45);
stagger(0.10) { show(clicks, 0.35); show(logins, 0.35); show(analytics, 0.35); }
show(queue, 0.40); show(dlq, 0.40);
show(processing.parts, 0.45); show(persist, 0.40);
show(storage.parts, 0.45); show(store, 0.40); show(dw, 0.40);
say(caption, "Docker engines process the stream; a queue, a dead-letter queue, a Lambda, and storage complete it.", 0.40);
}
}
wait(0.20);
step("possible-topology") {
par {
stagger(0.08) {
draw(parse, 0.4); draw(toEngines, 0.4); draw(toQueue, 0.4); draw(toHandlers, 0.4);
draw(toDlq, 0.4); draw(toStore, 0.4); draw(toDw, 0.4); draw(toSpark, 0.4);
}
show(legPipe, 0.3); show(legAna, 0.3); show(legStore, 0.3); show(legDlq, 0.3);
say(caption, "Every dashed edge is a possible relationship—not a claim that data is flowing.", 0.40);
}
}
wait(0.30);
step("ingress-flow") {
par {
seq { flow(parse, 1.0); flow(toSpark, 1.0); }
say(caption, "Logging → parse → Kafka → Spark: the ingest-and-analytics tap.", 0.40);
}
}
wait(0.20);
step("runtime-hot-path") {
par {
show(event.parts, 0.25);
hotpath(event, 6.0, 27);
say(caption, "One event chooses valid branches: Kafka → an engine → results queue → Lambda → storage.", 0.40);
}
}
wait(0.30);
step("dead-letter") {
par {
flow(toDlq, 1.2);
say(caption, "Messages the handler cannot process are diverted to the dead-letter queue.", 0.40);
}
}
wait(0.30);
step("takeaway") {
par {
pulse(flows.parts, 0.70);
pulse(event.parts, 0.60);
say(caption, "One mixed stack—on-prem ingest, serverless compute, cloud storage—stays readable end to end.", 0.45);
}
}
wait(1.40);
onprem-advanced-web-service
The on-prem stress test with native archetypes: Nginx, gRPC, Redis/PostgreSQL HA, Fluentd→Kafka→Spark, and Prometheus/Grafana — three runtime stories on one platform.
title("Advanced On-Premises Web Service — Runtime Paths");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.14, h*0.03), "Made With Manic");
text(kicker, (cx, h*0.04), "SYSTEMS KIT · ON-PREMISES WEB SERVICE");
text(headline, (cx, h*0.125), "One platform. Three runtime stories.");
text(caption, (cx, h*0.945), "First reveal ownership; then distinguish requests, telemetry, and the analytics stream.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 30*u); bold(headline); wrap(headline, w*0.86);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86);
if h > 1.25*w {
architecture(platform, (cx,h*0.52), w*0.84,h*0.72);
}
else {
architecture(platform, (cx,h*0.52), w*0.95,h*0.72);
}
node(ingress, platform, "gateway", "Nginx Ingress");
cluster(services, platform, "SERVICE CLUSTER");
node(grpc1, services, "service", "gRPC 1");
node(grpc2, services, "service", "gRPC 2");
node(grpc3, services, "service", "gRPC 3");
cluster(state, platform, "");
cluster(sessions, state, "SESSIONS HA");
node(sessionPrimary, sessions, "cache", "Redis Session");
node(sessionReplica, sessions, "cache", "Redis Replica");
cluster(databases, state, "DATABASE HA");
node(dbPrimary, databases, "database", "PostgreSQL Users");
node(dbReplica, databases, "database", "PostgreSQL Replica");
cluster(pipeline, platform, "EVENT ANALYTICS");
node(logging, pipeline, "service", "Fluentd Logging");
node(stream, pipeline, "queue", "Kafka Stream");
node(analytics, pipeline, "service", "Spark Analytics");
cluster(observability, platform, "OBSERVABILITY");
node(metrics, observability, "external", "Prometheus");
node(monitoring, observability, "external", "Grafana");
// Main request and dependency possibilities.
connect(ingressToServices, ingress, services);
connect(servicesToSessions, services, sessionPrimary, orthogonal);
connect(servicesToDatabase, services, dbPrimary, orthogonal);
connect(servicesToLogging, services, logging, orthogonal);
// The log stream remains one explicit pipeline.
connect(loggingToStream, logging, stream, orthogonal);
connect(streamToAnalytics, stream, analytics, orthogonal);
// Grafana queries Prometheus; Prometheus explicitly scrapes both replicas.
connect(dashboardQuery, monitoring, metrics, orthogonal);
connect(sessionScrape, metrics, sessionReplica, orthogonal);
connect(databaseScrape, metrics, dbReplica, orthogonal);
// HA replication is a neutral relationship, not a directed runtime claim.
link(sessionReplication, sessionPrimary.card, sessionReplica.card);
link(databaseReplication, dbPrimary.card, dbReplica.card);
dashed(sessionReplication); dashed(databaseReplication);
color(sessionReplication, dim); color(databaseReplication, dim);
stroke(sessionReplication, 2.5*u); stroke(databaseReplication, 2.5*u);
message(webRequest, ingress, "REQ");
message(sessionRead, grpc2, "S");
message(databaseRead, grpc3, "DB");
message(logEvent, grpc1, "LOG");
message(query, monitoring, "Q");
message(sessionProbe, metrics, "S");
message(databaseProbe, metrics, "D");
hidden(kicker); hidden(headline); hidden(caption);
hidden(platform.nodes);
hidden(services.parts); hidden(state.parts); hidden(sessions.parts); hidden(databases.parts);
hidden(pipeline.parts); hidden(observability.parts);
hidden(webRequest.parts); hidden(sessionRead.parts); hidden(databaseRead.parts);
hidden(logEvent.parts); hidden(query.parts); hidden(sessionProbe.parts); hidden(databaseProbe.parts);
untraced(platform.connections); untraced(sessionReplication); untraced(databaseReplication);
step("entry-and-services") {
par {
show(kicker, 0.30); show(headline, 0.45); show(caption, 0.40);
show(ingress, 0.35);
show(services.parts, 0.45);
stagger(0.08) { show(grpc1, 0.30); show(grpc2, 0.30); show(grpc3, 0.30); }
say(caption, "Nginx can reach three gRPC workers. The dashed fan-out is possibility, not a load-balancing simulation.", 0.40);
}
}
wait(0.20);
step("state-pipeline-and-monitoring") {
par {
show(state.parts, 0.45); show(sessions.parts, 0.40); show(databases.parts, 0.40);
show(sessionPrimary, 0.30); show(sessionReplica, 0.30);
show(dbPrimary, 0.30); show(dbReplica, 0.30);
show(pipeline.parts, 0.45); show(logging, 0.30); show(stream, 0.30); show(analytics, 0.30);
show(observability.parts, 0.45); show(metrics, 0.30); show(monitoring, 0.30);
say(caption, "Sessions, users, analytics, and observability keep separate ownership boundaries.", 0.40);
}
}
wait(0.20);
step("possible-topology") {
par {
draw(platform.connections, 1.10, smooth);
draw(sessionReplication, 0.55, smooth);
draw(databaseReplication, 0.55, smooth);
say(caption, "Straight, curved, and port-aware orthogonal paths still form one selectable topology.", 0.40);
}
}
wait(0.30);
step("one-ingress-request") {
par {
seq {
show(webRequest.parts, 0.18);
route(webRequest, ingressToServices, 1.00, linear);
}
say(caption, "One HTTP request selects one gRPC worker; the other possible ingress lanes remain cold.", 0.40);
}
}
wait(0.25);
step("parallel-state-dependencies") {
par {
fade(webRequest.parts, 0.20);
seq { show(sessionRead.parts, 0.18); route(sessionRead, servicesToSessions, 1.45, linear); }
seq { show(databaseRead.parts, 0.18); route(databaseRead, servicesToDatabase, 1.45, linear); }
flow(sessionReplication, 1.45, both, continuous);
flow(databaseReplication, 1.45, both, continuous);
say(caption, "Two workers can contact independent state systems while replication stays a neutral HA relationship.", 0.40);
}
}
wait(0.25);
step("logging-to-analytics") {
par {
fade(sessionRead.parts, 0.20); fade(databaseRead.parts, 0.20);
fade(servicesToSessions.hot, 0.25); fade(servicesToDatabase.hot, 0.25);
seq {
show(logEvent.parts, 0.18);
route(logEvent, servicesToLogging, 1.00, linear);
route(logEvent, loggingToStream, 0.85, linear);
route(logEvent, streamToAnalytics, 0.85, linear);
}
say(caption, "The same log event travels gRPC → Fluentd → Kafka → Spark without becoming a replacement dot.", 0.40);
}
}
wait(0.25);
step("observability") {
par {
fade(logEvent.parts, 0.20);
fade(servicesToLogging.hot, 0.25);
fade(loggingToStream.hot, 0.25);
fade(streamToAnalytics.hot, 0.25);
seq { show(query.parts, 0.18); route(query, dashboardQuery, 0.80, linear); }
seq { show(sessionProbe.parts, 0.18); route(sessionProbe, sessionScrape, 1.25, linear); }
seq { show(databaseProbe.parts, 0.18); route(databaseProbe, databaseScrape, 1.25, linear); }
say(caption, "Grafana queries Prometheus while two explicit scrape messages select the replica targets.", 0.40);
}
}
wait(0.35);
step("takeaway") {
par {
pulse(services.parts, 0.70); pulse(state.parts, 0.70);
pulse(pipeline.parts, 0.70); pulse(observability.parts, 0.70);
say(caption, "Ownership stays static. Persistent objects and authored paths explain what the platform does.", 0.45);
}
}
wait(1.30);
onprem-advanced-web-service-v2
The same platform with real provider icons and paths coloured by relationship (request · analytics · telemetry · replication).
title("Advanced On-Premises Web Service — Runtime Paths (with icons)");
canvas("16:9");
template("neon");
let u = (w+h-abs(w-h))/1440;
watermark(mark, (w*0.14, h*0.03), "Made With Manic");
text(kicker, (cx, h*0.04), "SYSTEMS KIT · ON-PREMISES WEB SERVICE");
text(headline, (cx, h*0.125), "One platform. Three runtime stories.");
text(caption, (cx, h*0.945), "First reveal ownership; then distinguish requests, telemetry, and the analytics stream.");
size(kicker, 16*u); color(kicker, dim); bold(kicker);
size(headline, 30*u); bold(headline); wrap(headline, w*0.86);
size(caption, 18*u); color(caption, dim); wrap(caption, w*0.86);
if h > 1.25*w {
architecture(platform, (cx,h*0.52), w*0.84,h*0.72);
}
else {
architecture(platform, (cx,h*0.52), w*0.95,h*0.72);
}
node(ingress, platform, "onprem:nginx", "Nginx Ingress");
cluster(services, platform, "SERVICE CLUSTER");
node(grpc1, services, "onprem:server", "gRPC 1");
node(grpc2, services, "onprem:server", "gRPC 2");
node(grpc3, services, "onprem:server", "gRPC 3");
cluster(state, platform, "");
cluster(sessions, state, "SESSIONS HA");
node(sessionPrimary, sessions, "onprem:redis", "Redis Session");
node(sessionReplica, sessions, "onprem:redis", "Redis Replica");
cluster(databases, state, "DATABASE HA");
node(dbPrimary, databases, "onprem:postgresql", "PostgreSQL Users");
node(dbReplica, databases, "onprem:postgresql", "PostgreSQL Replica");
cluster(pipeline, platform, "EVENT ANALYTICS");
node(logging, pipeline, "onprem:fluentd", "Fluentd Logging");
node(stream, pipeline, "onprem:kafka", "Kafka Stream");
node(analytics, pipeline, "onprem:spark", "Spark Analytics");
cluster(observability, platform, "OBSERVABILITY");
node(metrics, observability, "onprem:prometheus", "Prometheus");
node(monitoring, observability, "onprem:grafana", "Grafana");
// Main request and dependency possibilities.
connect(ingressToServices, ingress, services);
connect(servicesToSessions, services, sessionPrimary, orthogonal);
connect(servicesToDatabase, services, dbPrimary, orthogonal);
connect(servicesToLogging, services, logging, orthogonal);
// The log stream remains one explicit pipeline.
connect(loggingToStream, logging, stream, orthogonal);
connect(streamToAnalytics, stream, analytics, orthogonal);
// Grafana queries Prometheus; Prometheus explicitly scrapes both replicas.
connect(dashboardQuery, monitoring, metrics, orthogonal);
connect(sessionScrape, metrics, sessionReplica, orthogonal);
connect(databaseScrape, metrics, dbReplica, orthogonal);
// HA replication is a neutral relationship, not a directed runtime claim.
link(sessionReplication, sessionPrimary.card, sessionReplica.card);
link(databaseReplication, dbPrimary.card, dbReplica.card);
dashed(sessionReplication); dashed(databaseReplication);
color(sessionReplication, dim); color(databaseReplication, dim);
stroke(sessionReplication, 2.5*u); stroke(databaseReplication, 2.5*u);
// --- colour every path by its RELATIONSHIP (cold dashed line + hot overlay) ---
// request path = cyan · analytics pipeline = gold · telemetry/scrape = magenta.
color(ingressToServices, cyan); color(ingressToServices.hot, cyan);
color(servicesToSessions, cyan); color(servicesToSessions.hot, cyan);
color(servicesToDatabase, cyan); color(servicesToDatabase.hot, cyan);
color(servicesToLogging, gold); color(servicesToLogging.hot, gold);
color(loggingToStream, gold); color(loggingToStream.hot, gold);
color(streamToAnalytics, gold); color(streamToAnalytics.hot, gold);
color(dashboardQuery, magenta); color(dashboardQuery.hot, magenta);
color(sessionScrape, magenta); color(sessionScrape.hot, magenta);
color(databaseScrape, magenta); color(databaseScrape.hot, magenta);
// (replication links stay neutral dim — a relationship, not a runtime path)
// legend: relationship → colour
text(legReq, (w*0.30, h*0.175), "REQUEST"); size(legReq, 13*u); color(legReq, cyan); bold(legReq);
text(legAna, (w*0.44, h*0.175), "ANALYTICS"); size(legAna, 13*u); color(legAna, gold); bold(legAna);
text(legTel, (w*0.60, h*0.175), "TELEMETRY"); size(legTel, 13*u); color(legTel, magenta); bold(legTel);
text(legRep, (w*0.76, h*0.175), "REPLICATION"); size(legRep, 13*u); color(legRep, dim); bold(legRep);
hidden(legReq); hidden(legAna); hidden(legTel); hidden(legRep);
message(webRequest, ingress, "REQ");
message(sessionRead, grpc2, "S");
message(databaseRead, grpc3, "DB");
message(logEvent, grpc1, "LOG");
message(query, monitoring, "Q");
message(sessionProbe, metrics, "S");
message(databaseProbe, metrics, "D");
hidden(kicker); hidden(headline); hidden(caption);
hidden(platform.nodes);
hidden(services.parts); hidden(state.parts); hidden(sessions.parts); hidden(databases.parts);
hidden(pipeline.parts); hidden(observability.parts);
hidden(webRequest.parts); hidden(sessionRead.parts); hidden(databaseRead.parts);
hidden(logEvent.parts); hidden(query.parts); hidden(sessionProbe.parts); hidden(databaseProbe.parts);
untraced(platform.connections); untraced(sessionReplication); untraced(databaseReplication);
step("entry-and-services") {
par {
show(kicker, 0.30); show(headline, 0.45); show(caption, 0.40);
show(ingress, 0.35);
show(services.parts, 0.45);
stagger(0.08) { show(grpc1, 0.30); show(grpc2, 0.30); show(grpc3, 0.30); }
say(caption, "Nginx can reach three gRPC workers. The dashed fan-out is possibility, not a load-balancing simulation.", 0.40);
}
}
wait(0.20);
step("state-pipeline-and-monitoring") {
par {
show(state.parts, 0.45); show(sessions.parts, 0.40); show(databases.parts, 0.40);
show(sessionPrimary, 0.30); show(sessionReplica, 0.30);
show(dbPrimary, 0.30); show(dbReplica, 0.30);
show(pipeline.parts, 0.45); show(logging, 0.30); show(stream, 0.30); show(analytics, 0.30);
show(observability.parts, 0.45); show(metrics, 0.30); show(monitoring, 0.30);
say(caption, "Sessions, users, analytics, and observability keep separate ownership boundaries.", 0.40);
}
}
wait(0.20);
step("possible-topology") {
par {
draw(platform.connections, 1.10, smooth);
draw(sessionReplication, 0.55, smooth);
draw(databaseReplication, 0.55, smooth);
show(legReq, 0.3); show(legAna, 0.3); show(legTel, 0.3); show(legRep, 0.3);
say(caption, "Each path is coloured by relationship: request, analytics, telemetry, replication.", 0.40);
}
}
wait(0.30);
step("one-ingress-request") {
par {
seq {
show(webRequest.parts, 0.18);
route(webRequest, ingressToServices, 1.00, linear);
}
say(caption, "One HTTP request selects one gRPC worker; the other possible ingress lanes remain cold.", 0.40);
}
}
wait(0.25);
step("parallel-state-dependencies") {
par {
fade(webRequest.parts, 0.20);
seq { show(sessionRead.parts, 0.18); route(sessionRead, servicesToSessions, 1.45, linear); }
seq { show(databaseRead.parts, 0.18); route(databaseRead, servicesToDatabase, 1.45, linear); }
flow(sessionReplication, 1.45, both, continuous);
flow(databaseReplication, 1.45, both, continuous);
say(caption, "Two workers can contact independent state systems while replication stays a neutral HA relationship.", 0.40);
}
}
wait(0.25);
step("logging-to-analytics") {
par {
fade(sessionRead.parts, 0.20); fade(databaseRead.parts, 0.20);
fade(servicesToSessions.hot, 0.25); fade(servicesToDatabase.hot, 0.25);
seq {
show(logEvent.parts, 0.18);
route(logEvent, servicesToLogging, 1.00, linear);
route(logEvent, loggingToStream, 0.85, linear);
route(logEvent, streamToAnalytics, 0.85, linear);
}
say(caption, "The same log event travels gRPC → Fluentd → Kafka → Spark without becoming a replacement dot.", 0.40);
}
}
wait(0.25);
step("observability") {
par {
fade(logEvent.parts, 0.20);
fade(servicesToLogging.hot, 0.25);
fade(loggingToStream.hot, 0.25);
fade(streamToAnalytics.hot, 0.25);
seq { show(query.parts, 0.18); route(query, dashboardQuery, 0.80, linear); }
seq { show(sessionProbe.parts, 0.18); route(sessionProbe, sessionScrape, 1.25, linear); }
seq { show(databaseProbe.parts, 0.18); route(databaseProbe, databaseScrape, 1.25, linear); }
say(caption, "Grafana queries Prometheus while two explicit scrape messages select the replica targets.", 0.40);
}
}
wait(0.35);
step("takeaway") {
par {
pulse(services.parts, 0.70); pulse(state.parts, 0.70);
pulse(pipeline.parts, 0.70); pulse(observability.parts, 0.70);
say(caption, "Ownership stays static. Persistent objects and authored paths explain what the platform does.", 0.45);
}
}
wait(1.30);
Coming from Manim
For folks arriving from Manim: each of these is a well-known Manim tutorial scene written the Manic way — same idea, Manic’s vocabulary, usually far less code. The side-by-side translation guide is in Coming from Manim to Manic; these are the runnable files.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
graph-area-plot
A coords frame + two plots, a vline, riemann bars, and a band between curves — Manim’s GraphAreaPlot, static.
// ============================================================================
// GraphAreaPlot — the Manim tutorial scene, in Manic.
//
// A coordinate frame with two parabolas, vertical lines up to the first curve,
// Riemann rectangles under it, and a shaded region between the two curves.
//
// Coordinate frame: math origin (0,0) → screen (250,620); 130 px per x-unit,
// 92 px per y-unit. Every plot below shares that same origin + scale.
// ============================================================================
title("Manim vs Manic — GraphAreaPlot");
canvas(1280, 720);
template("black");
// axes over x∈[0,5], y∈[0,6] — no arrow tips, ticks numbered, axes named "x" / "y"
coords(ax, (250, 620), (0, 5), (0, 6), 130, 92, 0, 1, 1, "x", "y");
// the two curves
plot(c1, (250, 620), 130, 92, "4*x - x*x", (0, 4)); color(c1, blue); stroke(c1, 3); // 4x − x²
plot(c2, (250, 620), 130, 92, "0.8*x*x - 3*x + 4", (0, 4)); color(c2, green); stroke(c2, 3); // 0.8x² − 3x + 4
// dotted guide lines from the x-axis up to curve_1 at x = 2 and x = 3
vline(l1, c1, 2, yellow);
vline(l2, c1, 3, yellow);
// Riemann rectangles under curve_1 on [0.3, 0.6], bars of width 0.03
riemann(rm, c1, 0.3, 0.6, 0.03, green);
// the region between the two curves on the slice [2, 3] (curve_1 on top)
band(ar, c1, c2, grey, (2, 3)); opacity(ar, 0.5);
graph-area-plot-animated
The same curve toured live: a sliding tangent+slope, an area sweep + integral readout, re-selected domains, then riemann bars.
// ============================================================================
// GraphAreaPlot — the full tour. One curve, y = 4x − x², and the whole calculus
// toolkit moving over it, selecting different domains as it goes:
//
// 1. reveal the frame + curve
// 2. DERIVATIVE — a tangent slides along the curve, slope read live
// 3. INTEGRAL — the area sweeps open 0→4, the value climbs to 32/3
// 4. DOMAINS — the interval is re-selected: [0,1], then [1,3], then [3,4]
// 5. RIEMANN — approximate the selected [1,3] with rectangles
//
// Same declarative primitives as the static diagram — the animator only says
// what happens when.
// ============================================================================
title("GraphAreaPlot — the full tour");
canvas(1280, 720);
template("black");
coords(ax, (250, 620), (0, 5), (0, 6), 130, 92, 0, 1, 1, "x", "y");
plot(c1, (250, 620), 130, 92, "4*x - x*x", (0, 4)); color(c1, blue); stroke(c1, 3);
// tour props
tangent(tg, c1, 0.4, 150); color(tg, gold); stroke(tg, 2);
slope(sv, c1, 0.4, (22, -28)); color(sv, gold); size(sv, 24);
area(acc, c1, 0, 0); color(acc, cyan); // whole-domain area, sweeps
integral(iv, c1, 0, 0, (940, 150)); color(iv, cyan); size(iv, 30);
area(a01, c1, 0, 1); color(a01, cyan); // the three selected sub-domains
area(a13, c1, 1, 3); color(a13, gold);
area(a34, c1, 3, 4); color(a34, magenta);
riemann(rm, c1, 1, 3, 0.25, green); // Riemann on the chosen [1,3]
// initial states
hidden(ax); untraced(c1);
hidden(tg); hidden(sv); hidden(iv);
opacity(acc, 0.30);
opacity(a01, 0); opacity(a13, 0); opacity(a34, 0); opacity(rm, 0);
// ===== 1. reveal the frame + curve =====
show(ax, 0.7); wait(0.2);
draw(c1, 1.2); wait(0.4);
// ===== 2. DERIVATIVE — the tangent slides, its slope read live (4 → 0 → −4) =====
par { show(tg, 0.4); show(sv, 0.4); }
par { to(tg, x, 3.6, 3.2, smooth); to(sv, x, 3.6, 3.2, smooth); }
wait(0.3);
par { fade(tg, 0.4); fade(sv, 0.4); }
// ===== 3. INTEGRAL — the area sweeps open, the value climbs to 32/3 ≈ 10.67 =====
show(iv, 0.4);
par { to(acc, x, 4, 3.2, smooth); to(iv, x, 4, 3.2, smooth); }
wait(0.7);
par { fade(acc, 0.5); fade(iv, 0.4); }
// ===== 4. DIFFERENT DOMAINS — re-select the interval, one slice at a time =====
to(a01, opacity, 0.5, 0.5); wait(0.5); // [0, 1]
to(a13, opacity, 0.5, 0.5); wait(0.5); // [1, 3]
to(a34, opacity, 0.5, 0.5); wait(0.7); // [3, 4]
par { fade(a01, 0.5); fade(a34, 0.5); } // keep just [1, 3]
// ===== 5. RIEMANN — approximate the selected [1,3] with rectangles =====
par { fade(a13, 0.5); to(rm, opacity, 0.75, 0.7); }
wait(0.9);
custom-axis-labels
A radian sine on coords with xtick/ytick labelling ticks at an axis VALUE (π/2) and named axes — no pixel maths.
// ============================================================================
// Custom (non-numeric) axis + tick labels — no pixel maths anywhere.
//
// `coords` names the axes for you (xname / yname), and `xtick` / `ytick` place
// a label at an axis VALUE — the label is any text: a number, a word, or a
// symbol like "π/2". Here: a sine wave on a radian axis.
// ============================================================================
title("Custom axis labels");
canvas(1280, 720);
template("black");
// frame: x from 0 to ~2π, y from -1.4 to 1.4; ticks every π/2; named axes
coords(ax, (200, 380), (0, 6.6), (-1.4, 1.4), 150, 130, 0, 1.5708, 0, "θ", "sin θ");
plot(sine, (200, 380), 150, 130, "sin(x)", (0, 6.2832)); color(sine, cyan); stroke(sine, 3);
// x-axis ticks labelled in radians — text placed at each VALUE, not a pixel
xtick(qa, ax, 1.5708, "π/2");
xtick(qb, ax, 3.1416, "π");
xtick(qc, ax, 4.7124, "3π/2");
xtick(qd, ax, 6.2832, "2π");
// y-axis: just the extremes
ytick(yp, ax, 1, "1");
ytick(ym, ax, -1, "-1");
constant-area-rectangle
A rectangle rides y=25/x via boxto+mark; the corner slides but the area stays 25 — Manim’s always_redraw, without the updater.
// ============================================================================
// Constant-area rectangle under a hyperbola.
//
// A rectangle runs from the origin to a point (x, 25/x) on the curve y = 25/x.
// As the corner slides along the curve, the rectangle morphs from wide-and-short
// to tall-and-narrow — but its area (width × height = x · 25/x) stays fixed at 25.
// A dot rides the corner along the curve.
// ============================================================================
title("Constant-area rectangle");
canvas(1280, 720);
template("black");
coords(ax, (200, 640), (0, 10), (0, 10), 90, 55, 0, 2, 1, "x", "y");
plot(g, (200, 640), 90, 55, "25/x", (2.5, 10)); color(g, gold); stroke(g, 3);
boxto(box, g, 5); color(box, blue); // rectangle origin → (x, 25/x); area = x·(25/x)
mark(dot, g, 5); color(dot, fg); // a dot riding the corner along the curve
equation(inv, (760, 150), `x \cdot y = 25`, 30); color(inv, blue);
// the axes, curve and dot are up from the start; the box is CREATED (drawn into being)
untraced(box); hidden(inv);
wait(0.4);
draw(box, 1.3); // outline traces on as the fill rises bottom-up
wait(0.3);
show(inv, 0.5); // then reveal the invariant
wait(0.4);
// slide the corner along the curve — width and height trade off, area holds at 25
par { to(box, x, 10, 1.8, smooth); to(dot, x, 10, 1.8, smooth); } // wide & short
par { to(box, x, 2.5, 2.0, smooth); to(dot, x, 2.5, 2.0, smooth); } // tall & narrow
par { to(box, x, 5, 1.8, smooth); to(dot, x, 5, 1.8, smooth); } // back to the 5×5 square
wait(0.6);
moving-camera-follow
A dot walks a sine; followshot makes the 2D camera track it, then releases and restores the wide shot — Manim’s MovingCameraScene.
// ============================================================================
// A moving camera that follows a dot along a curve.
//
// The wide shot shows a sine wave with dots at both ends. The camera zooms in
// on a dot at the start, then TRACKS it as it travels the whole curve — a follow
// shot — before pulling back out to the wide shot.
// ============================================================================
title("Moving camera — follow shot");
canvas(1280, 720);
template("black");
coords(ax, (200, 480), (-1, 10), (-1, 10), 95, 45, 1, 2, 1, "x", "y");
plot(g, (200, 480), 95, 45, "sin(x)", (0, 9.4248)); color(g, blue); stroke(g, 3);
mark(d1, g, 0); color(d1, fg); // fixed dot at the start
mark(d2, g, 9.4248); color(d2, fg); // fixed dot at the end
mark(md, g, 0); color(md, orange); // the dot the camera will follow
wait(0.6);
// zoom in and glide to the dot at the start of the curve
par { zoom(2, 1.3, smooth); cam((200, 480), 1.3, smooth); }
followshot(md); // lock the camera onto the moving dot
to(md, x, 9.4248, 4.5, linear); // it walks the whole sine curve; the camera trails it
followshot(none); // release the lock
// pull back out to the wide shot
par { zoom(1, 1.3, smooth); cam((640, 360), 1.3, smooth); }
wait(0.5);
moving-highlight-box
The product rule written with mathparts (addressable terms), a framebox boxes one term, then surround glides it to the next.
// ============================================================================
// The product rule, with a highlight box that moves between the two terms.
//
// The equation is one `mathparts` — split into parts that lay themselves out and
// stay individually addressable (pr.0, pr.1, …). It's written on left-to-right,
// a box is drawn around the first product term, then the box glides + resizes to
// surround the second.
// ============================================================================
title("Product rule — moving highlight box");
canvas(1280, 720);
template("black");
mathparts(pr, (640, 340),
`\frac{d}{dx}f(x)g(x)=`, `f(x)\frac{d}{dx}g(x)`, `+`, `g(x)\frac{d}{dx}f(x)`, 34);
framebox(fb, pr.1, 10); color(fb, gold); // box the first product term (pr.1)
untraced(pr); untraced(fb);
wait(0.5);
// write the equation on, part by part, left to right
draw(pr.0, 0.9); draw(pr.1, 0.7); draw(pr.2, 0.3); draw(pr.3, 0.8);
wait(0.4);
draw(fb, 0.8); // draw the box around the first term
wait(0.9);
surround(fb, pr.3, 1.0, smooth); // move it to the second term (pr.3)
wait(0.8);
moving-angle
Two arms share a vertex; one swings and anglemark’s arc + its θ label track the opening live, then the label recolours — Manim’s MovingAngle.
// ============================================================================
// A moving angle: two arms share a vertex, one arm swings, and the angle arc
// plus its θ label track the opening live — no updaters, just `turn`.
//
// The whole rig is reactive: `segment` arms reflow to their endpoints, and
// `anglemark` recomputes its arc AND rides its θ label along the bisector
// every frame. Rotate the moving point and everything follows.
// ============================================================================
title("Moving angle");
canvas(1280, 720);
template("black");
point(o, (470, 470)); // vertex — the pivot everything turns about
point(pr, (800, 470)); // fixed arm, along the horizontal
point(pm, (357, 188)); // moving arm, starts at ~110°
segment(l1, o, pr);
segment(lm, o, pm);
anglemark(ang, pr, o, pm, "θ"); // arc + θ label, both track the opening
wait(0.6);
turn(pm, o, -70, 1.6); // close the angle down to ~40°
wait(0.5);
turn(pm, o, 140, 2.2); // swing wide, past straight
wait(0.4);
recolor(ang.label, red, 0.5); // paint the θ red
wait(0.4);
turn(pm, o, 170, 2.6); // carry on around
wait(0.6);
moving-dots
Two dots move on independent axes (slidex/slidey) joined by a segment that always connects them — Manim’s MovingDots.
// ============================================================================
// Two dots move on independent axes, joined by a line that always connects
// them. The blue dot slides right; the green dot rises; the red segment
// stretches to follow — reactively, with no updaters.
//
// `slidex`/`slidey` push one dot along a single axis (the other coordinate
// stays put), and `segment` reflows to its endpoints every frame.
// ============================================================================
title("Moving dots");
canvas(1280, 720);
template("black");
point(a, (440, 420)); // blue dot — will slide right
point(b, (640, 420)); // green dot — will rise
color(a, blue);
color(b, green);
segment(l, a, b); // the joining line, tracks both endpoints
color(l, red);
wait(0.5);
slidex(a, 840, 1.5); // blue dot travels right along its row
wait(0.3);
slidey(b, 180, 1.5); // green dot climbs its column
wait(0.6);
moving-group-to-destination
groupscale grows a tagged row about its centre, then dock slides it so one member lands on a target — Manim’s MovingGroupToDestination.
// ============================================================================
// A whole group of dots slides to a destination — rigidly, so that one chosen
// member (the red dot) lands exactly on a yellow target, carrying the rest of
// the row along with it.
//
// The four dots share a tag, so the whole row moves and scales as one:
// `groupscale` grows it about its collective centre, then `dock` slides it so
// the chosen member (the red dot) lands on the target. You name the member and
// the target; the shift is worked out for you — no delta arithmetic.
// ============================================================================
title("Moving group to destination");
canvas(1280, 720);
template("black");
point(d0, (360, 470)); tag(d0, row);
point(d1, (470, 470)); tag(d1, row);
point(d2, (580, 470)); tag(d2, row); // the chosen member
point(d3, (690, 470)); tag(d3, row);
color(d2, red);
point(dest, (940, 190));
color(dest, yellow);
wait(0.6);
groupscale(row, 1.4, 1.0); // grow the row about its own centre
wait(0.3);
dock(row, d2, dest, 1.6); // slide the row so d2 lands on dest
wait(0.5);
point-with-trace
A dot swings a half-circle then steps up/left; trail records the whole path behind it — sampled from the timeline, so it’s exact and seekable.
// ============================================================================
// A dot leaves a trail as it moves — first swinging a half-circle around a
// pivot, then stepping up and to the left. The gold path records everywhere
// the dot has been, growing behind it as it goes.
//
// `trail` samples the dot's trajectory once and replays it by time, so the
// path is exact and seekable — no per-frame bookkeeping.
// ============================================================================
title("Point with trace");
canvas(1280, 720);
template("black");
point(dot, (540, 380));
trail(tr, dot, gold, 3);
wait(0.4);
turn(dot, (740, 380), 180, 2.0, linear); // swing a half-circle about the pivot
wait(0.4);
shift(dot, (0, -160), 1.0); // step up
shift(dot, (-200, 0), 1.0); // step left
wait(0.6);
sin-and-cos-plot
sin & cos on one frame with graphlabel LaTeX labels pinned to each curve, plus a vline guide at x=2π — Manim’s SinAndCosFunctionPlot.
// ============================================================================
// sin and cos on one coordinate frame, each curve labelled where it lives, plus
// a vertical guide at x = 2π with its own label.
//
// `coords` gives the whole frame (ranges, ticks, numbers, axis names); the new
// piece is `graphlabel` — a LaTeX label that pins itself to a curve at an x and
// nudges clear of the line, taking the curve's colour by default.
// ============================================================================
title("sin and cos");
canvas(1280, 720);
template("black");
coords(ax, (640, 360), (-10, 10), (-1.5, 1.5), 52, 105, 0, 2, 1, "x", "y");
plot(sinq, (640, 360), 52, 105, "sin(x)", (-10, 10)); color(sinq, blue);
plot(cosq, (640, 360), 52, 105, "cos(x)", (-10, 10)); color(cosq, red);
graphlabel(sl, sinq, `\sin(x)`, -9, up); // rides sin near its left end
graphlabel(cl, cosq, `\cos(x)`); // default: cos's right end, up
vline(vl, cosq, 6.2832, yellow); // guide up to cos at x = 2π
graphlabel(ll, cosq, `x=2\pi`, 6.2832, upright, white);
moving-zoomed-scene
A loupe: a frame over the scene + a panel showing that region magnified LIVE; pan the frame and the panel tracks it — Manim’s ZoomedScene.
// ============================================================================
// A magnifier (loupe): a small frame sits over a detailed corner of the scene,
// and a panel shows that region blown up live. Move the frame and the panel
// tracks it — the magnified view is the real scene, redrawn zoomed.
// ============================================================================
title("Moving zoomed scene");
canvas(1280, 720);
template("black");
// --- a patch of fine detail to inspect ---
circle(a, (360, 300), 10); color(a, red);
circle(b, (400, 290), 7); color(b, cyan);
circle(c, (438, 306), 9); color(c, green);
circle(d, (410, 330), 5); color(d, gold);
text(cap, (400, 360), "detail", 16);
dot(spot, (400, 300), 4);
// --- the magnifier: frame over the patch, panel showing it 3.2x ---
loupe(lp, (400, 310), 150, 96, (900, 360), 3.2);
wait(0.4);
show(lp.panel);
wait(0.4);
shift(lp.frame, (-40, -30), 1.4); // pan the loupe — the panel follows live
wait(0.4);
shift(lp.frame, (60, 40), 1.4);
wait(0.6);
deform-homotopy
deform — a continuous homotopy: over its duration it remaps a shape’s outline by (u(x,y,t), v(x,y,t)) each frame. A wave ripples through a circle, then a twist shears a square and unwinds — Manim’s Homotopy/ApplyWave/ComplexHomotopy in one verb (the animated twin of warp).
// deform — a continuous homotopy: over its duration, `deform` animates t: 0→1 and
// remaps an entity's outline points by (u(x,y,t), v(x,y,t)) every frame. Manim's
// `Homotopy` / `ApplyWave` / `ComplexHomotopy` in one verb (the animated twin of
// the static `warp`). Write t=0 as the identity (`u=x`, `v=y`) for a clean start.
//
// manic examples/deform-homotopy.manic
title("Deform — a continuous homotopy");
canvas("16:9");
template("black");
circle(ring, (380, 330), 140); color(ring, cyan);
rect(box, (940, 330), 230, 230); color(box, gold);
text(narr, (640, 636), "One verb, any point-map of (x, y, t).", 24);
wait(0.6);
say(narr, "A wave ripples through a shape — v = y + amp·sin(x·k + t)·sin(π·t).");
deform(ring, "x", "y + 34*sin(x*0.05 + t*9)*sin(3.14159*t)", 2.8, smooth);
wait(1.6);
say(narr, "A twist — shear that grows from centre, then unwinds. Same verb.");
deform(box, "x + (y-330)*0.45*sin(3.14159*t)", "y - (x-940)*0.45*sin(3.14159*t)", 2.8, smooth);
wait(2.0);
say(narr, "Author the identity at t=0, and any continuous deformation follows.");
wait(2.0);
first-class-shapes
Every core shape owns its FILL and its OUTLINE — each with its own colour and opacity — and DRAWS ITSELF ON. Circle, rect, polygon, ellipse, line, arrow: solid, hollow (outlined), or a translucent fill under a crisp rim (opacity(id, v, fill)), each revealed with untraced+draw — Manim’s stylable Mobject + Create, native.
// First-class shapes — EVERY core Manic shape owns its FILL and its OUTLINE (each
// its own colour + opacity) and DRAWS ITSELF ON. Not just circles: rect, polygon,
// ellipse, line, arrow — all the same first-class treatment. All native Manic: the
// ctors are unchanged, style is the usual t=0 modifiers, the reveal is untraced+draw.
//
// color(id, c) → the FILL colour outline(id, c) → the RING colour
// opacity(id, v, fill) → the FILL opacity opacity(id, v, stroke) → the RING opacity
// outlined(id) → hollow (fill off) filled(id) → solid (fill on)
// untraced(id); draw(id) → the shape draws itself on (Manim's Create)
//
// Closed shapes (circle/rect/polygon/ellipse) have a fill you can make translucent
// or drop; open paths (line/arrow) are stroke-only but still draw themselves on.
//
// manic examples/manim-vs/first-class-shapes.manic
title("First-class shapes");
canvas("16:9");
template("black");
// ── closed shapes: fill + outline, three postures ───────────────────────────
// SOLID filled — draws on, fill washes in behind the pen.
circle(cc, (220, 250), 72); color(cc, cyan);
untraced(cc);
// HOLLOW ring — outlined() drops the fill; outline() colours the ring.
rect(rc, (490, 250), 160, 120); outline(rc, gold); outlined(rc);
untraced(rc);
// TRANSLUCENT fill under a crisp rim — opacity(id,v,fill) touches only the interior.
polygon(tri, (720, 185), (802, 320), (638, 320)); color(tri, magenta); outline(tri, white);
opacity(tri, 0.45, fill); opacity(tri, 1.0, stroke);
untraced(tri);
// ELLIPSE is a stroke-only curve (no fill layer), so `color` sets its stroke — a
// naturally hollow conic. Draws on like any path.
ellipse(el, (1010, 250), 96, 60, 0); color(el, lime); stroke(el, 3);
untraced(el);
// ── open paths: stroke-only, but they draw on too ───────────────────────────
line(seg, (170, 470), (440, 470)); color(seg, cyan); stroke(seg, 3);
untraced(seg);
arrow(arw, (560, 510), (860, 430)); color(arw, gold);
untraced(arw);
text(cap, (640, 610), "every shape owns its fill + outline, and draws itself on", 22);
hidden(cap);
// ── timeline: verbs only. Watch each shape create. ──────────────────────────
wait(0.4);
show(cap);
wait(0.8);
say(cap, "Closed shapes: solid, hollow, translucent fill — each drawn on.");
draw(cc);
wait(0.5);
draw(rc);
wait(0.5);
draw(tri);
wait(0.5);
draw(el);
wait(1.2);
say(cap, "Open paths draw on too — a line, an arrow.");
par { draw(seg); draw(arw); }
wait(1.2);
// once drawn they're ordinary styled shapes — every verb still works (kit rule).
say(cap, "Then the usual verbs keep working — pulse, flash.");
par { pulse(tri); flash(cc); }
wait(1.4);
show-styles
The reveal family on one page: a plain show fade-in, a grow-in from an anchor (hidden(id, from) → show), and a pen draw-on (untraced+draw) — how a shape first appears is a base-setter + verb pair, not a special effect.
// The show family — reveal styles. `show` brings an entity onto the scene; the
// STYLE is chosen by how you `hidden` it first (the base-setter), exactly like
// `untraced` arms `draw`. Plain `hidden(id)` → fade-in; `hidden(id, from)` arms a
// GROW-IN from an anchor: `center` | `up`/`down`/`left`/`right` (a bbox edge) |
// `tail` (an arrow's start) | a point `(x,y)`. Manim's FadeIn + GrowFromCenter/
// Edge/Arrow/Point, all under one reveal verb.
//
// manic examples/manim-vs/show-styles.manic
title("The show family — reveal styles");
canvas("16:9");
template("black");
circle(s1, (220, 300), 58); color(s1, cyan);
circle(s2, (450, 300), 58); color(s2, lime);
rect(s3, (680, 300), 116, 116); color(s3, gold);
arrow(s4, (880, 360), (1020, 235)); color(s4, magenta);
circle(s5, (1150, 300), 58); color(s5, #ff8c1a);
text(narr, (640, 600), "The show family — one verb, many reveal styles.", 24);
// how you HIDE it picks the reveal style:
hidden(s1); // plain → fade-in (FadeIn)
hidden(s2, center); // from centre → pop in (GrowFromCenter)
hidden(s3, up); // from top edge → unfold (GrowFromEdge)
hidden(s4, tail); // from arrow tail → extend (GrowArrow)
hidden(s5, (1150, 520)); // from a point → fly + grow (GrowFromPoint)
hidden(narr);
wait(0.4);
show(narr);
wait(1.6);
say(narr, "show — a fade-in. (Manim FadeIn)");
show(s1, 0.7);
wait(1.5);
say(narr, "hidden(center) + show — grow from the centre. (GrowFromCenter)");
show(s2, 0.7);
wait(1.6);
say(narr, "hidden(up) — unfold from a bounding edge. (GrowFromEdge)");
show(s3, 0.7);
wait(1.6);
say(narr, "hidden(tail) on an arrow — grow from the tail to the tip. (GrowArrow)");
show(s4, 0.7);
wait(1.6);
say(narr, "hidden((x,y)) — grow from any point. (GrowFromPoint)");
show(s5, 0.8);
wait(1.8);
say(narr, "The style lives on `hidden` — like `untraced` arms `draw`.");
wait(2.0);
indication
The attention flourishes — pulse, flash, wiggle, blink, circumscribe, spotlight, passflash — each a temporary emphasis that returns the shape to rest, on any entity or tagged group (Manim’s Indicate / Flash / Circumscribe / FocusOn family).
// Attention cues — ways to draw the eye to an object, each returning to its base
// state. Alongside the existing `pulse` (a scale bump) and `flash` (a radial
// burst), this adds `wiggle` (scale + rotate jiggle), `circumscribe` (a line
// drawn round it, then gone), `spotlight` (a disc converges onto it), `blink`,
// and `passflash` (a light travels the outline).
// Manim's Indicate / Flash / Wiggle / Circumscribe / FocusOn / Blink / ShowPassingFlash.
//
// manic examples/manim-vs/indication.manic
title("Attention cues — indicate an object");
canvas("16:9");
template("black");
circle(a, (240, 320), 56); color(a, cyan);
rect(b, (520, 320), 112, 112); color(b, gold);
circle(c, (800, 320), 56); color(c, lime);
equation(d, (1050, 315), `E=mc^2`, 30); color(d, magenta);
text(narr, (640, 560), "Five ways to draw the eye — each returns to base.", 24);
hidden(narr);
wait(0.5);
show(narr);
wait(1.4);
say(narr, "wiggle — a scale-and-rotate jiggle (the rotate cousin of shake).");
wiggle(a, 0.8);
wait(1.4);
say(narr, "circumscribe — a line drawn round it, then gone.");
circumscribe(b, magenta, 1.1);
wait(1.4);
say(narr, "spotlight — a translucent disc converges onto it.");
spotlight(c, 1.4);
wait(1.4);
say(narr, "blink — flash out and in.");
blink(d, 0.8);
wait(1.4);
say(narr, "passflash — a light travels the outline.");
passflash(b, cyan, 1.3);
wait(1.6);
spiral-cycle-restore
Three position moves: spiralin reveals a group by spiralling its members into place, cycle slides each entity to the next’s spot along an arc, and savestate+restore remembers home and snaps back — Manim’s SpiralIn / CyclicReplace / save_state+Restore.
// Three motion moves: `spiralin` reveals a group by spiralling its members into
// place; `cycle` rotates positions along an arc (each slides to the next's spot);
// `savestate` + `restore` remembers where each started and snaps it home. Shown on
// two rows so each reads cleanly. Manim's SpiralIn / CyclicReplace / save_state + Restore.
//
// manic examples/manim-vs/spiral-cycle-restore.manic
title("Spiral in · cycle · restore");
canvas("16:9");
template("black");
// top row — spiralin reveal
circle(a1, (470, 210), 34); color(a1, cyan);
circle(a2, (570, 210), 34); color(a2, lime);
circle(a3, (670, 210), 34); color(a3, gold);
circle(a4, (770, 210), 34); color(a4, magenta);
tag(a1, top); tag(a2, top); tag(a3, top); tag(a4, top);
// bottom row — cycle, then restore to home
circle(b1, (500, 450), 40); color(b1, cyan);
circle(b2, (640, 450), 40); color(b2, lime);
circle(b3, (780, 450), 40); color(b3, gold);
savestate(b1); savestate(b2); savestate(b3); // remember each home
text(narr, (640, 630), "Reveal, rearrange, return.", 22);
hidden(narr);
wait(0.4);
show(narr);
wait(1.2);
say(narr, "spiralin — the top group spirals into place, staggered.");
spiralin(top, 1.6);
wait(1.4);
say(narr, "cycle — each bottom dot slides to the next's spot along a 90° arc.");
cycle(b1, b2, b3, 1.3, 90);
wait(1.6);
say(narr, "restore — savestate remembered home, so each snaps back.");
par { restore(b1, 0.8); restore(b2, 0.8); restore(b3, 0.8); }
wait(1.6);
circle-area-proof
The area-of-a-circle LIMIT proof: each stage is a fresh disc CUT into N wedges and ANIMATED — every wedge spins about its tip and slides into a tip-to-tail strip. 4 → 8 → 16 → 32 flattens the humps toward a πr × r rectangle, area πr² throughout. Every strip is for-loop generated from the tessellation formulas (adv = r·sin(π/N), drop = r·cos(π/N)).
// Circle area = πr², the LIMIT proof. Each stage is a fresh disc that is CUT into
// N wedges and then ANIMATED — every wedge spins about its tip and slides into a
// tip-to-tail strip. 4 → 8 → 16 → 32: with more wedges the humps shrink and the
// strip flattens toward a πr × r rectangle. Area πr² all along.
//
// A disc cut into N equal sectors, alternated up/down, tessellates: consecutive
// tips advance r·sin(π/N) and the up/down tip-lines sit r·cos(π/N) apart. Wedge k
// starts at angle k·(360/N); to point up it spins to centre 270°, to point down 90°.
// Everything is a `for` loop over those formulas — no hand-placed geometry.
//
// manic examples/manim-vs/circle-area-proof.manic
title("Area of a circle = πr²");
canvas("16:9");
template("black");
text(hdr, (640, 66), "Cut a disc into wedges, re-lay them — flatter each time — Manic", 26);
// ---- BUILD every stage's wedges as an assembled disc at (640,300), hidden -----
let sw = 90.0;
for k in 0..4 { sector(w4{k}, (640,300), 150, k*sw, sw); filled(w4{k}); hue(w4{k}, 190 + (k - 2*floor(k/2))*140); tag(w4{k}, g4); }
let sw = 45.0;
for k in 0..8 { sector(w8{k}, (640,300), 150, k*sw, sw); filled(w8{k}); hue(w8{k}, 190 + (k - 2*floor(k/2))*140); tag(w8{k}, g8); }
let sw = 22.5;
for k in 0..16 { sector(w16{k}, (640,300), 150, k*sw, sw); filled(w16{k}); hue(w16{k}, 190 + (k - 2*floor(k/2))*140); tag(w16{k}, g16); }
let sw = 11.25;
for k in 0..32 { sector(w32{k}, (640,300), 150, k*sw, sw); filled(w32{k}); hue(w32{k}, 190 + (k - 2*floor(k/2))*140); tag(w32{k}, g32); }
hidden(g4); hidden(g8); hidden(g16); hidden(g32);
text(cap, (640, 690), "", 24); hidden(cap);
wait(0.3); show(cap);
// ---- ANIMATE each stage: show the cut disc, then spin+slide into the strip ----
// stage 4
let half = 45.0; let ar = pi/4; let adv = 150*sin(ar); let drop = 150*cos(ar); let x0 = 640 - 3*adv/2;
say(cap, "4 wedges — spin them up/down and slide them tip-to-tail.");
show(g4, 0.6); wait(1.0);
par { for k in 0..4 { move(w4{k}, (x0 + k*adv, 440 - (k - 2*floor(k/2))*drop), 1.8, smooth); spin(w4{k}, 270 - (k - 2*floor(k/2))*180 - (2*k+1)*half, 1.8); } }
wait(1.6); fade(g4, 0.5);
// stage 8
let half = 22.5; let ar = pi/8; let adv = 150*sin(ar); let drop = 150*cos(ar); let x0 = 640 - 7*adv/2;
say(cap, "8 wedges — flatter already.");
show(g8, 0.6); wait(0.9);
par { for k in 0..8 { move(w8{k}, (x0 + k*adv, 440 - (k - 2*floor(k/2))*drop), 1.7, smooth); spin(w8{k}, 270 - (k - 2*floor(k/2))*180 - (2*k+1)*half, 1.7); } }
wait(1.5); fade(g8, 0.5);
// stage 16
let half = 11.25; let ar = pi/16; let adv = 150*sin(ar); let drop = 150*cos(ar); let x0 = 640 - 15*adv/2;
say(cap, "16 wedges — the humps shrink.");
show(g16, 0.6); wait(0.9);
par { for k in 0..16 { move(w16{k}, (x0 + k*adv, 440 - (k - 2*floor(k/2))*drop), 1.7, smooth); spin(w16{k}, 270 - (k - 2*floor(k/2))*180 - (2*k+1)*half, 1.7); } }
wait(1.5); fade(g16, 0.5);
// stage 32 — the limit
let half = 5.625; let ar = pi/32; let adv = 150*sin(ar); let drop = 150*cos(ar); let x0 = 640 - 31*adv/2;
say(cap, "32 wedges — a πr × r rectangle. Area = π r².");
show(g32, 0.6); wait(0.9);
par { for k in 0..32 { move(w32{k}, (x0 + k*adv, 440 - (k - 2*floor(k/2))*drop), 1.7, smooth); spin(w32{k}, 270 - (k - 2*floor(k/2))*180 - (2*k+1)*half, 1.7); } }
wait(2.6);
3Blue1Brown, reproduced
Real 3Blue1Brown lessons rebuilt in Manic — the math visualised and narrated with Manic captions. From the Essence of Linear Algebra to the Riemann zeta function, each is a single readable .manic file you can open, run, and adapt.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
vectors-basis-transformations
Essence of Linear Algebra, chapters 1–11 in one file: vectors → basis → the grid transforming (gridmap) → composition → determinant → inverse → the dot product (projection AND duality) → change of basis → eigenvectors → eigenbasis → functions as vectors.
// ============================================================================
// Essence of Linear Algebra, chapters 1→2→3 in one arc.
//
// Ch1 A vector is an arrow from the origin; its coordinates are [x, y].
// Ch2 Those coordinates are SCALARS: v = x·î + y·ĵ — a linear combination of
// the basis vectors î=(1,0), ĵ=(0,1).
// Ch3 A matrix is a transformation: it says where î and ĵ LAND, and the whole
// grid morphs to follow (`gridmap` animates it). Because v = x·î + y·ĵ,
// it rides along, landing at x·(new î) + y·(new ĵ).
//
// Example: v = [1, 2] under [[2,1],[1,2]] (î→(2,1), ĵ→(1,2)), so v lands at
// 1·(2,1) + 2·(1,2) = (4,5). Origin (400,470), 55 px/unit.
// ============================================================================
title("Vectors, basis, and transformations");
canvas(1280, 720);
template("black");
text(narr, (640, 696), "A vector is an arrow from the origin.", 24);
// the plane + basis, ready to be transformed in Ch3 (starts at the identity)
gridmap(gm, (400, 470), 55, 2, 1, 1, 2, 5);
// the vector v = [1,2] and its linear-combination scaffold
vector(v, (400, 470), (55, 110), yellow);
vector(jj, (455, 470), (0, 110), red); // 2·j-hat, stacked on i-hat → reaches v
equation(vlab, (486, 352), `\vec{v}=[1,2]`, 30); color(vlab, yellow);
equation(ilab, (472, 500), `\hat{\imath}`, 26); color(ilab, lime);
equation(jlab, (366, 414), `\hat{\jmath}`, 26); color(jlab, red);
equation(mtx, (980, 165), `\begin{bmatrix}2&1\\1&2\end{bmatrix}`, 44);
// Ch4 — a second transform (90° rotation) composed on top: morph M1 → M2·M1
gridmap(gm2, (400, 470), 55, -1, -2, 2, 1, 5, 2, 1, 1, 2);
equation(comp4, (640, 96), `\begin{bmatrix}0&-1\\1&0\end{bmatrix}\begin{bmatrix}2&1\\1&2\end{bmatrix}=\begin{bmatrix}-1&-2\\2&1\end{bmatrix}`, 34);
// the determinant cell (Ch5), the projection (Ch7), and the alt-basis grid (Ch8)
determinant(dt, (400, 470), 55, 2, 1, 1, 2, gold);
project(pr, (400, 470), 55, (1, 2), (3, 1)); // project v=(1,2) onto span(w=(3,1))
squish(sq, (400, 470), 55, 1, 2, 5); // Ch7 duality: dot with v = squish onto a line
eigen(ev, (400, 470), 55, 2, 1, 1, 2, gold); // Ch9 eigenvectors of [[2,1],[1,2]] (λ = 3, 1)
diagonalise(dg, (400, 470), 55, 2, 1, 1, 2, gold); // Ch10 eigenbasis = a pure stretch
coords(cf, (700, 450), (-4, 4), (-3, 3), 46, 46, 0); // Ch11 a function graph…
plot(fn, (700, 450), 46, 46, "0.25*x*x*x - x", (-3.6, 3.6)); color(fn, mint);
equation(deq, (700, 130), `\tfrac{d}{dx}\;\leftrightarrow\;\begin{bmatrix}0&1&0\\0&0&2\\0&0&0\end{bmatrix}`, 32);
gridmap(cb, (400, 470), 55, 1, 0.7, 0.4, 1, 5);
vector(vb, (400, 470), (55, 110), yellow);
// --- initial visibility ---
hidden(gm.i); hidden(gm.j); // basis revealed in Ch2
hidden(v); hidden(vlab); hidden(jj); hidden(ilab); hidden(jlab); hidden(mtx);
hidden(dt); hidden(dt.unit); hidden(dt.val); hidden(pr);
hidden(sq); hidden(sq.line); hidden(sq.dual);
hidden(ev); hidden(dg); hidden(cf); hidden(fn); hidden(deq);
hidden(gm2); hidden(gm2.bg); hidden(gm2.i); hidden(gm2.j); hidden(comp4);
hidden(cb); hidden(cb.bg); hidden(cb.i); hidden(cb.j); hidden(vb);
// --- Ch1: a vector ---
wait(0.5);
par { show(v); show(vlab); }
wait(1.3);
// --- Ch2: coordinates are a linear combination of the basis ---
say(narr, "Its coordinates are scalars: v = 1·i-hat + 2·j-hat.");
par { show(gm.i); show(gm.j); show(ilab); show(jlab); }
wait(0.7);
show(jj); // i-hat then two j-hats reach v
wait(1.6);
// --- Ch3: the matrix transforms space; v rides along ---
say(narr, "A matrix moves i-hat and j-hat — and the whole grid follows.");
show(mtx);
par { fade(jj); fade(ilab); fade(jlab); fade(vlab); }
par { to(gm, morph, 1, 2.2); grow(v, (620, 195), 2.2); } // space deforms, v follows
wait(0.6);
say(narr, "v lands at 1·(new i-hat) + 2·(new j-hat) = (4,5).");
wait(1.6);
// --- Ch4: matrix multiplication is composition ---
say(narr, "Ch 4 — apply one transform, then another: that is matrix multiplication.");
show(comp4); // the product, clearly placed up top
par { fade(gm); fade(v); }
show(gm2); // starts at M1 (seamless with Ch3)
to(gm2, morph, 1, 1.8); // now rotate 90°: the grid is at M2·M1
wait(1.3);
to(gm2, morph, 0, 1.5); // undo the second transform, back to M1
par { fade(gm2); fade(comp4); }
par { show(gm); show(v); }
wait(0.6);
// --- Ch5: the determinant is the area scale factor ---
say(narr, "Ch 5 — the determinant is how much areas scale: 2·2 − 1·1 = 3.");
par { show(dt); show(dt.unit); show(dt.val); }
wait(1.9);
par { fade(dt); fade(dt.unit); fade(dt.val); } // fade = timeline hide (hidden is base-state only)
// --- Ch6: column space + the inverse undoes the transform ---
say(narr, "Ch 6 — the columns span the whole plane, so the inverse sends space back.");
par { to(gm, morph, 0, 1.8); grow(v, (455, 360), 1.8); } // reverse the morph = the inverse
wait(1.4);
// --- Ch7: the dot product — projection, and its dual (a squish onto a line) ---
say(narr, "Ch 7 — the dot product of v and w is a projection — w's shadow on v.");
par { fade(gm); fade(gm.bg); fade(v); fade(mtx); }
show(pr);
wait(1.6);
say(narr, "It's also a squish of the whole plane onto a line — the dual vector IS v.");
fade(pr);
par { show(sq); show(sq.line); show(sq.dual); }
to(sq, morph, 1, 1.9); // collapse space onto the number line
wait(1.0);
to(sq, morph, 0, 1.2); // and back — the dual vector remains
wait(0.8);
// --- Ch8: change of basis ---
say(narr, "Ch 8 — change of basis: the same arrow, read on a different grid.");
par { fade(sq); fade(sq.line); fade(sq.dual); }
par { show(cb); show(vb); }
to(cb, morph, 1, 1.8); // the coordinate grid changes; v stays put
wait(1.8);
// --- Ch9: eigenvectors and eigenvalues ---
say(narr, "Ch 9 — some vectors keep to their own line: eigenvectors (λ = 3 and λ = 1).");
par { fade(cb); fade(vb); }
show(ev);
wait(2.0);
// --- Ch10: the eigenbasis is a pure stretch ---
say(narr, "Ch 10 — in the eigenbasis, the transformation is just a diagonal stretch.");
fade(ev);
show(dg);
wait(2.0);
// --- Ch11: abstract vector spaces ---
say(narr, "Ch 11 — even functions are vectors; the derivative is a linear map with a matrix.");
fade(dg);
par { show(cf); show(fn); show(deq); }
wait(2.2);
duality-dot-product
The dot product as DUALITY: the plane collapses onto a number line under a 1×2 map (squish), and the gold dual vector is what you dot with.
// ============================================================================
// The dot product and duality (eola chapter 7 — the "why" behind chapter8p2).
//
// A 1x2 matrix [a b] is a linear map from the plane to a NUMBER LINE: it sends
// (x,y) to a·x + b·y and squishes all of space onto the line. Every such map
// corresponds to a single 2D vector — the DUAL vector (a,b) — and applying the
// map is exactly the dot product with (a,b). That correspondence is duality.
//
// Built on the new `squish` builtin. Here [a b] = [2 1].
// ============================================================================
title("The dot product and duality");
canvas(1280, 720);
template("black");
text(narr, (640, 694), "A 1×2 matrix is a map from the plane to a number line.", 24);
equation(mtx, (980, 130), `\begin{bmatrix}2&1\end{bmatrix}`, 44);
squish(sq, (640, 360), 70, 2, 1, 4);
// --- reveal ---
hidden(sq.dual); hidden(mtx);
wait(0.6);
show(mtx);
wait(1.2);
say(narr, "It squishes all of space onto the line — i-hat to 2, j-hat to 1.");
to(sq, morph, 1, 2.4);
wait(1.2);
say(narr, "Every such map corresponds to a single 2D vector — the dual vector (2,1).");
to(sq, morph, 0, 1.6); // un-squish, back to the plane
show(sq.dual);
wait(1.4);
say(narr, "Applying the map is the same as dotting with (2,1). That is duality.");
pulse(sq.dual);
wait(1.8);
warp-grid
A complex-plane grid DEFORMS under z → z² (warp) — lines bend into the classic parabolic conformal web.
// ============================================================================
// A coordinate grid over the complex plane, deforming under z -> f(z).
//
// `warp` samples each grid line through the complex function, so straight lines
// bend into curves. It starts as the identity grid and morphs to the warped
// image when animated — `to(id, morph, 1, dur)`. Here z -> z^2, the classic
// conformal map (the non-linear twin of `linmap`, and the deforming-grid
// companion to `domaincolor`).
// ============================================================================
title("Complex grid warp");
canvas(1280, 720);
template("black");
warp(g, (640, 360), 82, `z*z`, 3, 40);
equation(fn, (640, 80), `z \mapsto z^{2}`, 46);
text(narr, (640, 672), "A grid over the complex plane.", 26);
wait(0.6);
say(narr, "Every point z moves to z squared — lines bend into parabolas.");
to(g, morph, 1, 2.6);
wait(1.6);
zeta
The Riemann zeta function by domaincolor (with a real zeta(z) in the complex evaluator): the pole at s=1, the critical line Re=½, and the first non-trivial zeros marked.
// ============================================================================
// The Riemann zeta function ζ(s), visualised by domain colouring.
//
// ζ is a genuine complex function now (`zeta(z)` in the complex evaluator, via
// Borwein's accelerated eta series — valid through the critical strip). Domain
// colouring paints the complex plane: hue = phase of ζ(s), brightness = |ζ(s)|,
// so ZEROS are dark points and the POLE at s=1 is bright. Every non-trivial
// zero found sits on the critical line Re = 1/2 — the Riemann Hypothesis.
// ============================================================================
title("The Riemann zeta function");
canvas(1280, 720);
template("black");
// the s-plane, coloured by ζ(s) — a tall strip up the imaginary axis so the
// first non-trivial zeros (½ ± 14.13i) are in frame
domaincolor(zp, (500, 330), 240, 640, `zeta(z)`, 5.4);
colorwheel(cw, (980, 470), 72); // phase → hue legend
equation(def, (960, 150), `\zeta(s)=\sum \frac{1}{n^{s}}`, 40);
text(narr, (640, 700), "Colour = phase of zeta; brightness = size.", 24);
// the critical line Re = 1/2 and the strip 0 < Re < 1
line(cl, (511, 12), (511, 648)); color(cl, gold); dashed(cl);
text(cllab, (700, 250), "critical line Re = 1/2", 20); color(cllab, gold);
// the first non-trivial zeros, on the critical line
dot(z1, (511, 16)); color(z1, gold);
dot(z2, (511, 644)); color(z2, gold);
text(zlab, (690, 60), "zeta = 0 (½ ± 14.13i)", 20); color(zlab, gold);
// the pole at s = 1
circle(pole, (522, 330), 10); outlined(pole); color(pole, cyan);
text(plab, (720, 330), "pole at s = 1", 20); color(plab, cyan);
// --- reveal ---
hidden(cl); hidden(cllab); hidden(z1); hidden(z2); hidden(zlab);
hidden(pole); hidden(plab); hidden(def); hidden(cw);
wait(0.4);
show(zp);
say(narr, "The Riemann zeta function, 1 + 1/2^s + 1/3^s + ...");
show(def);
wait(1.2);
say(narr, "Defined for Re(s) > 1, then continued everywhere but a pole at s = 1.");
par { show(pole); show(plab); }
wait(1.2);
say(narr, "Its zeros are the dark points.");
show(cw);
wait(1.0);
say(narr, "Every non-trivial zero found lies on the line Re = 1/2.");
par { show(cl); show(cllab); }
wait(0.7);
par { show(z1); show(z2); show(zlab); pulse(z1); pulse(z2); }
wait(1.6);
euler-characteristic
Euler’s formula V − E + F = 2 on a wheel graph: V and E counted, faces filled + counted by regions, and 6 − 10 + 6 = 2 assembled.
// ============================================================================
// Euler's characteristic formula, V - E + F = 2, on a planar graph.
//
// The graph is a wheel W5 (a hub joined to a 5-point rim). We count its
// vertices, its edges, and its faces — the faces detected and filled by
// `regions` (5 inner triangles), plus the one outer face — and watch
// V - E + F land on 2.
// ============================================================================
title("Euler's characteristic");
canvas(1280, 720);
template("black");
equation(thm, (760, 70), `V - E + F = 2`, 46);
// --- the wheel graph W5 ---
polygon(rim, (760,150),(560,295),(637,530),(883,530),(960,295));
color(rim, cyan);
line(k1, (760,360),(760,150)); tag(k1, spokes); color(k1, cyan);
line(k2, (760,360),(560,295)); tag(k2, spokes); color(k2, cyan);
line(k3, (760,360),(637,530)); tag(k3, spokes); color(k3, cyan);
line(k4, (760,360),(883,530)); tag(k4, spokes); color(k4, cyan);
line(k5, (760,360),(960,295)); tag(k5, spokes); color(k5, cyan);
regions(cells, rim, spokes); // fills + counts the 5 inner faces
dot(hub,(760,360));
dot(p1,(760,150)); dot(p2,(560,295)); dot(p3,(637,530));
dot(p4,(883,530)); dot(p5,(960,295));
// --- the running tally, on the left ---
equation(tv, (250, 235), `V = 6`, 44);
equation(te, (250, 345), `E = 10`, 44);
equation(tf, (250, 455), `F = 6`, 44);
text(fnote, (250, 500), "5 inside + 1 outside", 18);
equation(sum, (760, 660), `6 - 10 + 6 = 2`, 52);
// --- reveal ---
untraced(rim); untraced(k1); untraced(k2); untraced(k3); untraced(k4); untraced(k5);
untraced(cells);
hidden(hub); hidden(p1); hidden(p2); hidden(p3); hidden(p4); hidden(p5);
hidden(tv); hidden(te); hidden(tf); hidden(fnote); hidden(sum);
wait(0.4);
par { draw(rim, 1.0); draw(k1, 1.0); draw(k2, 1.0); draw(k3, 1.0); draw(k4, 1.0); draw(k5, 1.0); }
par { show(hub); show(p1); show(p2); show(p3); show(p4); show(p5); }
wait(0.4);
// count the vertices
par { show(tv); pulse(hub); pulse(p1); pulse(p2); pulse(p3); pulse(p4); pulse(p5); }
wait(0.5);
// count the edges
par { show(te); pulse(rim); pulse(k1); pulse(k2); pulse(k3); pulse(k4); pulse(k5); }
wait(0.5);
// count the faces — 5 filled by `regions`, plus the outer face
draw(cells, 1.3);
par { show(tf); show(fnote); }
wait(0.8);
// and the characteristic lands on 2
show(sum);
wait(1.4);
euler-proof
The spanning-tree / dual-tree PROOF of V − E + F = 2, built on spantree (run on the primal AND the dual) and dual.
// ============================================================================
// Why V - E + F = 2 — the spanning-tree / dual-tree proof, narrated by Manic.
//
// A spanning tree of the graph uses V-1 edges (every vertex, no loop). The
// edges it leaves out are in bijection with the bounded faces: their duals form
// a spanning tree of the DUAL graph, using F-1 edges. Every edge is in exactly
// one of the two trees, so (V-1) + (F-1) = E, i.e. V - E + F = 2.
//
// Built on `spantree` (run on the primal AND the dual) and `dual`.
// ============================================================================
title("Euler's formula — a proof");
canvas(1280, 720);
template("black");
equation(thm, (300, 66), `V - E + F = 2`, 42);
text(narr, (760, 682), "A connected planar graph.", 26);
// --- the wheel graph W5 ---
polygon(rim, (760,150),(560,295),(637,530),(883,530),(960,295));
color(rim, cyan);
line(k1, (760,360),(760,150)); tag(k1, spokes); color(k1, cyan);
line(k2, (760,360),(560,295)); tag(k2, spokes); color(k2, cyan);
line(k3, (760,360),(637,530)); tag(k3, spokes); color(k3, cyan);
line(k4, (760,360),(883,530)); tag(k4, spokes); color(k4, cyan);
line(k5, (760,360),(960,295)); tag(k5, spokes); color(k5, cyan);
regions(cells, rim, spokes); // the F faces (faint context)
opacity(cells, 0.18);
dot(hub,(760,360));
dot(p1,(760,150)); dot(p2,(560,295)); dot(p3,(637,530));
dot(p4,(883,530)); dot(p5,(960,295));
// tally
equation(tv, (135, 200), `V = 6`, 34);
equation(te, (135, 275), `E = 10`, 34);
equation(tf, (135, 350), `F = 6`, 34);
// the two trees (declared now, revealed in sequence)
spantree(pt, spokes, rim); // primal tree {pt} (green) + co-tree {pt.co} (orange)
dual(du, rim, spokes); // dual: {du} edges + {du.nodes} dots
spantree(dt, du); // dual's spanning tree {dt}
color(dt, teal);
equation(sum, (760, 632), `(V-1) + (F-1) = E`, 40);
equation(nums, (760, 632), `5 + 5 = 10`, 40);
// --- initial visibility ---
untraced(rim); untraced(k1); untraced(k2); untraced(k3); untraced(k4); untraced(k5);
untraced(cells); untraced(pt); untraced(pt.co); untraced(dt);
hidden(hub); hidden(p1); hidden(p2); hidden(p3); hidden(p4); hidden(p5);
hidden(tv); hidden(te); hidden(tf);
hidden(du); hidden(du.nodes); hidden(dt.co); // substrate + dual co-tree stay hidden
hidden(sum); hidden(nums);
// 1. the graph
wait(0.4);
par { draw(rim,1.0); draw(k1,1.0); draw(k2,1.0); draw(k3,1.0); draw(k4,1.0); draw(k5,1.0); }
par { show(hub); show(p1); show(p2); show(p3); show(p4); show(p5); }
wait(0.6);
// 2. count V, E, F
say(narr, "V vertices, E edges, F faces.");
draw(cells, 0.9);
par { show(tv); show(te); show(tf); }
wait(1.0);
// 3. a spanning tree — V-1 edges
say(narr, "A spanning tree: every vertex, no loops — V-1 = 5 edges.");
par { draw(pt, 1.4); }
wait(1.0);
// 4. the leftover (co-tree) edges
say(narr, "That leaves E - (V-1) = 5 edges.");
par { draw(pt.co, 1.0); }
wait(1.0);
// 5. the dual — one node per face
say(narr, "The dual graph: one node inside each face.");
par { show(du.nodes); }
wait(1.0);
// 6. a spanning tree of the dual — F-1 edges
say(narr, "Its spanning tree pairs with the leftovers — F-1 = 5 edges.");
par { draw(dt, 1.4); }
wait(1.0);
// 7. the two trees tile every edge
say(narr, "Every edge lies in exactly one tree.");
show(sum);
wait(1.0);
par { fade(sum); show(nums); }
wait(1.0);
// 8. conclusion
say(narr, "So V - E + F = 2.");
pulse(thm);
wait(1.6);
pythagorean-proof
The dissection proof of a² + b² = c²: two identical (a+b)-squares, four triangles each, leaving c² on one and a²+b² on the other.
// ============================================================================
// The Pythagorean theorem by dissection, a^2 + b^2 = c^2.
//
// Two identical squares of side (a+b), each filled with four copies of the same
// right triangle. On the left the triangles leave a tilted square on the
// hypotenuse — c^2. On the right, the same four triangles regroup to leave a
// square on each leg — a^2 and b^2. Same square, same triangles, so a^2+b^2=c^2.
//
// a = 120, b = 180 (so the tilted square's side is c = sqrt(120^2+180^2)).
// ============================================================================
title("Pythagoras by dissection");
canvas(1280, 720);
template("black");
equation(thm, (640, 96), `a^2 + b^2 = c^2`, 50);
text(narr, (640, 672), "Two identical squares, side a + b.", 26);
// ---- LEFT square: four triangles + c^2 ----
polygon(sqL, (120,210),(420,210),(420,510),(120,510)); outlined(sqL); color(sqL, dim);
// four corner triangles (legs a,b)
polygon(la1, (120,210),(240,210),(120,390), teal);
polygon(la2, (420,210),(420,330),(240,210), teal);
polygon(la3, (420,510),(300,510),(420,330), teal);
polygon(la4, (120,510),(120,390),(300,510), teal);
opacity(la1, 0.4); opacity(la2, 0.4); opacity(la3, 0.4); opacity(la4, 0.4);
// the tilted square on the hypotenuse
polygon(csq, (240,210),(420,330),(300,510),(120,390), gold);
equation(clab, (270, 360), `c^2`, 40);
// ---- RIGHT square: the same four triangles + a^2 + b^2 ----
polygon(sqR, (600,210),(900,210),(900,510),(600,510)); outlined(sqR); color(sqR, dim);
// a^2 (top-left) and b^2 (bottom-right)
polygon(asq, (600,210),(720,210),(720,330),(600,330), blue);
polygon(bsq, (720,330),(900,330),(900,510),(720,510), violet);
equation(alab, (660, 270), `a^2`, 34);
equation(blab, (810, 420), `b^2`, 40);
// the four triangles filling the two leftover rectangles
polygon(ra1, (720,210),(900,210),(900,330), teal);
polygon(ra2, (720,210),(900,330),(720,330), teal);
polygon(ra3, (600,330),(720,330),(600,510), teal);
polygon(ra4, (720,330),(720,510),(600,510), teal);
opacity(ra1, 0.4); opacity(ra2, 0.4); opacity(ra3, 0.4); opacity(ra4, 0.4);
// side = a + b, under each square
brace(brL, (120,528),(420,528), "down"); text(brLt, (270,556), "a + b", 20);
brace(brR, (600,528),(900,528), "down"); text(brRt, (750,556), "a + b", 20);
// ---- reveal ----
hidden(la1); hidden(la2); hidden(la3); hidden(la4);
untraced(csq); hidden(clab);
untraced(asq); untraced(bsq); hidden(alab); hidden(blab);
hidden(ra1); hidden(ra2); hidden(ra3); hidden(ra4);
hidden(brL); hidden(brLt); hidden(brR); hidden(brRt);
wait(0.4);
par { show(sqL); show(sqR); }
par { show(brL); show(brR); show(brLt); show(brRt); }
wait(0.7);
say(narr, "Fill each with four copies of the same right triangle.");
par { show(la1); show(la2); show(la3); show(la4); show(ra1); show(ra2); show(ra3); show(ra4); }
wait(0.9);
say(narr, "On the left they leave a square on the hypotenuse: c squared.");
draw(csq, 1.0); show(clab);
wait(1.0);
say(narr, "Rearranged, the same four leave a square on each leg: a squared, b squared.");
par { draw(asq, 0.9); draw(bsq, 0.9); }
par { show(alab); show(blab); }
wait(1.1);
say(narr, "Same square, same four triangles — so a squared plus b squared is c squared.");
pulse(thm);
wait(0.6);
par { pulse(csq); pulse(asq); pulse(bsq); }
wait(1.4);
region-partition
regions fills + counts the areas a pentagram’s chords cut a disk into (16) — the planar-arrangement engine behind Moser/Euler/dissection scenes.
// ============================================================================
// Region partition: five points on a circle, every chord drawn between them,
// and `regions` fills + counts the areas they cut the disk into. The pentagon
// + its diagonals (a pentagram) partition the circle into 16 regions.
//
// `regions(id, boundary, dividers)` computes the planar arrangement of the
// boundary and every divider, then fills each enclosed face — the areas are
// detected for you, no coordinates for the pieces.
// ============================================================================
title("Region partition");
canvas(1280, 720);
template("black");
circle(disk, (640, 360), 240);
// five points on the circle
dot(pa, (640, 120)); dot(pb, (412, 286)); dot(pc, (499, 554));
dot(pd, (781, 554)); dot(pe, (868, 286));
// all ten chords (5 sides + 5 diagonals), tagged so `regions` takes them at once
line(s0, (640,120), (412,286)); tag(s0, chords);
line(s1, (412,286), (499,554)); tag(s1, chords);
line(s2, (499,554), (781,554)); tag(s2, chords);
line(s3, (781,554), (868,286)); tag(s3, chords);
line(s4, (868,286), (640,120)); tag(s4, chords);
line(g0, (640,120), (499,554)); tag(g0, chords);
line(g1, (640,120), (781,554)); tag(g1, chords);
line(g2, (412,286), (781,554)); tag(g2, chords);
line(g3, (412,286), (868,286)); tag(g3, chords);
line(g4, (499,554), (868,286)); tag(g4, chords);
regions(rg, disk, chords); // fills + counts every enclosed area (16)
light-and-brachistochrone
The Brachistochrone’s optics arc: refract (real Snell’s law + angle sweep) → the native brachistochrone race where the cycloid wins.
// ============================================================================
// Light and the brachistochrone — the optics half of the 2016 video.
//
// Bernoulli solved "what curve gives the fastest slide?" by a leap: a sliding
// bead is like a ray of light, which always takes the FASTEST path. Light
// bends when it enters a slower medium (Snell's law); a bead falling through
// ever-faster layers bends the same way — and the limiting curve is a cycloid.
//
// Built on the optics kit's `refract` (real Snell's law, angle sweep) and the
// physics kit's `brachistochrone` (four beads race, the cycloid wins).
// ============================================================================
title("Light and the brachistochrone");
canvas(1280, 720);
template("black");
text(narr, (640, 694), "Which path through the water is fastest?", 24);
refract(rf, (640, 360), 1.0, 1.5); // air over glass/water — light bends
equation(snell, (640, 84), `\frac{\sin\theta_1}{v_1}=\frac{\sin\theta_2}{v_2}`, 40);
brachistochrone(bc, (450, 230)); // A→B racing curves (shown in act 2)
// --- act 1: light refracts ---
hidden(snell); hidden(bc);
wait(0.6);
say(narr, "Crossing into water light slows, and bends toward the normal.");
wait(1.6);
say(narr, "Steeper in means steeper out — always by Snell's law.");
run(rf, 3.2);
show(snell);
wait(1.2);
// --- act 2: Bernoulli's leap → the cycloid ---
say(narr, "Bernoulli's leap: a sliding bead is like light through faster and faster layers.");
par { fade(rf); fade(snell); }
wait(0.8);
show(bc);
say(narr, "Its fastest path is a cycloid — the brachistochrone.");
run(bc, 3.6);
wait(1.6);
snell-to-cycloid
The derivation that connects them: layers of ever-faster media, light bending at each boundary (Snell), the zig-zag smoothing into a cycloid where sin θ/√y is constant.
// ============================================================================
// Snell's law → the cycloid — the derivation that connects light to the
// brachistochrone (3b1b's multilayered.py).
//
// Slice the medium into horizontal layers, each one letting light move faster
// than the last (a falling bead speeds up: v = sqrt(y)). Light bends at every
// boundary by Snell's law, sin(θ)/v = const. The bent path zig-zags down; as
// the layers get thinner it smooths into a single curve — a CYCLOID — on which
// sin(θ)/sqrt(y) is constant everywhere. That curve is the brachistochrone.
//
// The bend angles below are the real Snell solution: v_i = sqrt(i+1),
// sin(θ_i) = sin(20°)·v_i, so θ steepens 20° → 57° from the vertical.
// ============================================================================
title("Snell's law becomes a cycloid");
canvas(1280, 720);
template("black");
text(narr, (640, 694), "A falling bead speeds up — so slice the fall into faster and faster layers.", 22);
// six layers, each faster (lighter) than the one above
rect(l0, (640, 178), 1280, 77); color(l0, indigo); opacity(l0, 0.32);
rect(l1, (640, 255), 1280, 77); color(l1, blue); opacity(l1, 0.32);
rect(l2, (640, 332), 1280, 77); color(l2, blue); opacity(l2, 0.42);
rect(l3, (640, 409), 1280, 77); color(l3, teal); opacity(l3, 0.42);
rect(l4, (640, 486), 1280, 77); color(l4, teal); opacity(l4, 0.55);
rect(l5, (640, 563), 1280, 77); color(l5, cyan); opacity(l5, 0.55);
// the discrete light path — bends once per boundary (Snell at each)
line(s0, (200,140), (228,217)); tag(s0, ray); color(s0, gold);
line(s1, (228,217), (270,294)); tag(s1, ray); color(s1, gold);
line(s2, (270,294), (327,371)); tag(s2, ray); color(s2, gold);
line(s3, (327,371), (399,448)); tag(s3, ray); color(s3, gold);
line(s4, (399,448), (491,525)); tag(s4, ray); color(s4, gold);
line(s5, (491,525), (609,602)); tag(s5, ray); color(s5, gold);
// the smooth limit: a cycloid through the same endpoints
param(cyc, (200,140), 130, 231, `t - sin(t)`, `cos(t) - 1`, (0, pi)); color(cyc, yellow);
// the invariant, and one angle to illustrate it
equation(inv, (960, 250), `\frac{\sin\theta}{\sqrt{y}} = \text{const}`, 40);
line(nrm, (274,320), (274,430)); color(nrm, dim); dashed(nrm); // vertical (normal)
line(tan, (240,300), (312,428)); color(tan, red); // tangent along the curve
text(thlab, (300, 340), "θ", 26); color(thlab, red);
// --- reveal ---
untraced(s0); untraced(s1); untraced(s2); untraced(s3); untraced(s4); untraced(s5);
untraced(cyc);
hidden(inv); hidden(nrm); hidden(tan); hidden(thlab);
wait(0.6);
par { show(l0); show(l1); show(l2); show(l3); show(l4); show(l5); }
wait(0.8);
say(narr, "Light bends at every boundary — Snell's law, sin(θ)/v constant.");
draw(s0, 0.4); draw(s1, 0.4); draw(s2, 0.4); draw(s3, 0.4); draw(s4, 0.4); draw(s5, 0.4);
wait(0.8);
say(narr, "Thinner and thinner layers — the zig-zag smooths into one curve.");
par { fade(ray); draw(cyc, 1.4); }
wait(1.0);
say(narr, "A cycloid — and along it sin(θ)/sqrt(y) stays constant everywhere.");
par { show(inv); show(nrm); show(tan); show(thlab); }
wait(1.8);
cross-product-3d
The 3D CROSS product (cross3): v × w perpendicular to the parallelogram v,w span, its length = that area, orbited to reveal the geometry behind the determinant formula.
// ============================================================================
// The cross product in 3D (eola chapter8p2 — the geometric part).
//
// v × w is the vector PERPENDICULAR to the plane of v and w, whose LENGTH is
// the area of the parallelogram they span (and whose sign follows the right
// hand). That geometry is what the î-ĵ-k̂ determinant formula computes.
//
// Example: v=(2,1,0), w=(-1,2,0), so v×w=(0,0,5) — straight up, length 5 = the
// parallelogram's area (2·2 − 1·(−1)). Built on the new `cross3` builtin.
// ============================================================================
title("The cross product in 3D");
canvas(1280, 720);
template("black");
text(narr, (640, 694), "Two vectors v and w in three-dimensional space.", 24);
equation(det, (960, 130),
`\vec v\times\vec w=\det\!\begin{bmatrix}\hat\imath&\hat\jmath&\hat k\\2&1&0\\-1&2&0\end{bmatrix}`, 30);
cross3(cr, (0, 0, 0), (2, 1, 0), (-1, 2, 0));
camera3((5.5, -5.5, 5.5), (0.2, 0.4, 1.6));
// --- reveal ---
hidden(cr.p); hidden(cr.e1); hidden(cr.e2); hidden(det);
wait(0.6);
wait(1.4); // v (green) and w (red) are up
say(narr, "They span a parallelogram.");
par { show(cr.e1); show(cr.e2); }
wait(1.4);
say(narr, "Their cross product v × w points perpendicular to both.");
show(cr.p);
wait(1.2);
say(narr, "Its length equals the area of that parallelogram.");
orbit3(75, 22, 9.5, 3.2); // orbit to reveal the perpendicular
wait(1.2);
say(narr, "That geometry is exactly what the determinant formula computes.");
show(det);
wait(1.8);
mnist-network
Neural Networks ch.1 & 3: a stylised digit (digit) feeds a real 144→16→16→10 network — the pixels ARE the inputs — then the forward pass computes, loss compares to the true label, and backward sends gradients back. The network builds layer by layer, not fully-formed.
// ============================================================================
// What IS a neural network — and how does it learn? 3Blue1Brown's Neural
// Networks (2017), chapters 1 & 3, as one journey:
//
// build — a network is layers of neurons joined by weighted connections,
// revealed one layer at a time (not dumped fully-formed);
// forward — a handwritten digit feeds in, each pixel an input, values flowing
// forward through the layers to ten output probabilities;
// loss — compare the guess with the true label;
// backward— backpropagation sends the error back as gradients, layer by layer.
//
// A stylised "5" (`tensor`) feeds a real deterministic `network` via `feed`,
// which flattens the pixels into the 144 inputs AND traces the lit strokes into
// the input column. Every value shown is computed, not decorative.
// ============================================================================
title("What is a neural network — and how it learns");
canvas("16:9");
template("black");
// the handwritten digit as pixel brightnesses (12×12 = 144 inputs).
// Change "5" to any 0-9 to feed a different digit — or draw your own shape with
// `tensor(... `#`=ink `.`=blank ...)`.
digit(img, (250, 340), "5", 18, cyan);
// 144 pixels → 16 → 16 → 10 digit classes (the real MNIST shape, abbreviated ⋮)
network(net, (830, 340), "144 16 16 10", "sigmoid sigmoid softmax", 620, 500, 7);
text(narr, (640, 672), "A handwritten digit is just a grid of pixel brightnesses.", 24);
hidden(img);
hidden(net);
hidden(narr);
wait(0.4);
show(narr);
show(img);
wait(1.8);
// --- BUILD: the network appears one layer at a time (the journey) ---
say(narr, "A network is layers of neurons, joined by weighted connections.");
show(net.layer0);
wait(0.7);
par { show(net.transition0); show(net.layer1); }
wait(0.5);
par { show(net.transition1); show(net.layer2); }
wait(0.5);
par { show(net.transition2); show(net.layer3); }
par { show(net.probabilities); show(net.status); }
wait(1.0);
// --- FORWARD: the digit feeds in and values flow forward ---
say(narr, "Feed the digit in: each pixel is an input, and the values flow forward.");
feed(net, img, 3.6, smooth);
wait(0.6);
// --- LOSS: compare the guess with the true label ---
say(narr, "Compare the output with the true label — a five.");
loss(net, "0 0 0 0 1 0 0 0 0 0", crossentropy, 1.6);
wait(0.8);
// --- BACKWARD: backpropagation sends the error back as gradients ---
say(narr, "Backpropagation sends the error back through the layers, as gradients.");
backward(net, 3.4, smooth);
wait(1.0);
say(narr, "Forward to predict, backward to learn — that is how a network trains.");
wait(2.0);
gradient-partials
How a network learns: f(x,y) as a cost landscape; slice3 holds one variable constant so a cross-section’s steepness IS the partial derivative; gradient3 stacks them into ∇f (uphill); descend3 rolls a ball down −∇f into the valley — with a movie camera that swings to reveal the stack.
// ============================================================================
// How a neural network learns — the gradient, and gradient descent. 3Blue1Brown's
// "Gradient descent" segment (gradient.py), the full arc:
//
// cost — a network's cost is a function of MANY weights; picture it as a
// landscape over just two;
// partials — to read ∂f/∂y, HOLD x constant → the surface slices to one curve,
// and its steepness IS the partial (slice3); likewise ∂f/∂x;
// gradient — stack the two partials → ∇f, the direction of steepest ascent;
// descent — to LEARN, step the other way: follow −∇f DOWNHILL until the ball
// settles in a valley — a minimum of the cost (descend3).
//
// Narrated with captions (no character rigs). Every curve, slope, and step is
// computed from the real surface.
// ============================================================================
title("How a neural network learns: gradient descent");
canvas("16:9");
template("black");
camera3((6, -8.5, 5.5), (0, 0, 0.7), 40);
axes3(world, (0, 0, 0), 2.4);
surface3(f, "exp(-x*x + cos(2*y))", (-2, 2), (-2, 2), 34);
opacity(f, 0.42);
slice3(sx, f, x, 0.6, 0.4, #ff3ea5); // hold x → cross-section along y (∂f/∂y)
slice3(sy, f, y, 0.4, 0.6, #ffb020); // hold y → cross-section along x (∂f/∂x)
gradient3(grad, f, 0.6, 0.4);
descend3(desc, f, 0.7, 0.35, 0.25, 45, #7cff3e);
equation(feq, (232, 96), `f(x,y)=e^{-x^2+\cos 2y}`, 28);
equation(ceq, (232, 150), `\text{cost } C(w_1,\dots,w_n)`, 24); color(ceq, dim);
equation(geq, (1040, 632), `\nabla f=\begin{bmatrix}\partial f/\partial x\\ \partial f/\partial y\end{bmatrix}`, 28);
text(narr, (640, 668), "A network's cost is one number over millions of weights.", 24);
hidden(sx); hidden(sx.slope);
hidden(sy); hidden(sy.slope);
hidden(grad); hidden(geq); hidden(narr);
hidden(desc); hidden(desc.ball);
untraced(desc); untraced(desc.ball); // start un-drawn so the descent can roll on
// A MOVIE camera runs concurrently with the story: it swings around and up so the
// stacked slices + gradient are never hidden behind the peak, then drops low to
// follow the ball rolling into the valley. (orbit3 composes in `par`.)
par {
seq {
orbit3(-18, 26, 9.5, 5.0, smooth); // establish: front-left, slightly high
orbit3(40, 36, 9.2, 6.5, smooth); // swing right & rise to reveal the STACK
orbit3(62, 22, 8.4, 6.0, smooth); // drop low, come round to watch the descent
orbit3(74, 24, 8.4, 4.5, smooth); // settle on the valley
}
seq {
wait(0.5);
show(narr);
show(ceq);
wait(2.2);
say(narr, "Picture it as a landscape over just two of them.");
wait(2.0);
say(narr, "To read ∂f/∂y, hold x constant — that slices the surface to one curve.");
par { show(sx); show(sx.slope); }
wait(2.4);
say(narr, "Hold y constant instead, and this slice's steepness is ∂f/∂x.");
par { show(sy); show(sy.slope); }
wait(2.4);
say(narr, "Stack the two partials and you get the gradient — steepest ascent.");
par { show(grad); show(geq); }
wait(2.8);
say(narr, "To LEARN, step the other way: downhill, along −∇f.");
par { show(desc); show(desc.ball); }
par { draw(desc, 3.0); draw(desc.ball, 3.0); } // the ball rolls, the trail draws
wait(0.6);
say(narr, "It rolls into a valley — a minimum of the cost. That is learning.");
pulse(desc.ball);
wait(2.6);
}
}
high-dimensions
Thinking outside the 10-D box: a point in N-dimensional space drawn as N dials (sliders). Put it on the unit sphere and Σxᵢ²=1 is a fixed budget — spread evenly every coordinate is tiny, and the corner sits √N away.
// ============================================================================
// Thinking in high dimensions — 3Blue1Brown's "10-dimensional box" device: a
// point in N-dimensional space is just N numbers, drawn as N dials. You can't
// picture 7 axes, but you CAN watch 7 sliders move together.
//
// Put the point on the unit sphere and the coordinates obey x₁²+…+x₇² = 1 — a
// fixed budget of "real estate" spread across the axes. Spread it evenly and
// every coordinate is tiny (1/√7); pile it into a few and the rest go slack.
// The corner (1,1,…,1) sits √7 away — and √N in N-D — which is why high
// dimensions feel so vast.
//
// `sliders` is the rack; `setsliders` drives all dials at once and shows Σxᵢ².
// ============================================================================
title("Thinking in high dimensions");
canvas("16:9");
template("black");
sliders(pt, 7, (640, 340), 820, 360, gold);
text(narr, (640, 656), "A point in 7-D space is just seven numbers — one dial each.", 24);
hidden(narr);
wait(0.4);
show(narr);
wait(2.4);
say(narr, "Put it on the unit sphere: x₁² + … + x₇² = 1 — a fixed budget.");
setsliders(pt, "0.38 0.38 0.38 0.38 0.38 0.38 0.38", 1.6);
wait(2.2);
say(narr, "Spread evenly, every coordinate is tiny — only 1 over root 7.");
wait(2.2);
say(narr, "Pile the budget into a few, and the rest fall slack — Σx² stays 1.");
setsliders(pt, "0.86 0.42 0.22 0.13 0.10 0.06 0.04", 1.6);
wait(2.4);
say(narr, "Swing it around — the total never leaves the sphere.");
setsliders(pt, "-0.30 0.55 -0.62 0.28 0.34 -0.18 0.12", 1.6);
wait(2.2);
say(narr, "And the corner (1,…,1) sits √7 away — √N in N-D. High space is vast.");
setsliders(pt, "1 1 1 1 1 1 1", 1.8);
wait(2.6);
Manic Platform REST API
The Manic Platform API is the hosted control plane around the portable Manic
engine. Use it to validate source, manage projects and API keys, run Manic AI,
and submit hosted renders. Your .manic source remains the portable artifact.
Open the interactive reference at docs.maniclang.com/api. It includes the request forms, response schemas, scoped authentication, and a downloadable OpenAPI document.
Start safely
- Create a scoped key from Manic Account.
- Use a
mk_test_…key while integrating; usemk_live_…only for production. - Send it as
Authorization: Bearer mk_live_…(orX-API-Key). - For render and AI creation requests, always generate an
Idempotency-Keyand retry with that same key after an uncertain network failure.
curl https://api.maniclang.com/v1/check \
-H 'Authorization: Bearer mk_test_…' \
-H 'Content-Type: application/json' \
--data '{"source":"title(\\"Hello\\"); canvas(\\"16:9\\");"}'
POST /v1/check is synchronous and uses no credits. Hosted rendering and AI
are asynchronous: submit, then poll GET /v1/jobs/{id} until the job reaches a
terminal state. The interactive reference lists only shipped public endpoints;
it never exposes render-host or provisioning endpoints.
For the architectural contract, resource lifecycle, retention, and error semantics, see the repository’s platform API contract.
Manic API reference
The complete index of every Manic builtin — 490 builtins across 19 kits. Each entry shows its signature, whether it sets things up or animates them, and what it does. Every name listed here is one the editor knows too, so autocomplete and highlighting always match this page.
Reading a signature
new idis the name created by a constructor.id or tagtargets an existing entity, generated part, or group.- Parameters inside brackets are optional.
…means the builtin is catalogued here, but its detailed parameters are covered in the topic chapter for its kit.- This is the exhaustive name index; the topic chapters remain the best place to learn composition, constraints, and examples.
Standard shapes, styling, and motion
| API | Kind | What it does |
|---|---|---|
circle(id: new id, center: (x, y), r: number) | constructor / setup | a circle |
rect(id: new id, center: (x, y), w: number, h: number) | constructor / setup | a rectangle |
image(id: new id, center: (x, y), path: string, [w: number], [h: number]) | constructor / setup | a raster image (PNG/JPG) from a bundled asset: URI or file path, centred at a point (w×h px) |
svg(id: new id, center: (x, y), path: string, [size: number]) | constructor / setup | import VECTOR artwork from an asset: URI or file path as native path entities (each subpath a traceable Polyline/Polygon), fitted to size px wide and centred; pieces are tagged {id} so draw/show/fade/hue/gradient/move broadcast |
equation(id: new id, center: (x, y), latex: string, [size: number]) | constructor / setup | typeset a LaTeX math string centred at a point; standard \textcolor{cyan}{…} gives individual terms semantic Manic colours that follow the template. Put LaTeX in BACKTICKS; animate with show/fade/move/scale, and untraced+draw gives a left-to-right WRITE-ON wipe |
mathparts(…) | constructor / setup | mathparts(id, (x,y), part0, part1, …, [size]) — one equation split into PARTS, auto-laid-out left-to-right + baseline-aligned, centred at (x,y). Each part is its own entity {id}.0/{id}.1/… (all tagged {id}) so you can framebox/color/draw a single term. Manim’s indexable MathTex(…). Put each part in BACKTICKS; a trailing number is the size |
framebox(id: new id, target: id or tag, [buff: number], [color: colour]) | constructor / setup | an outlined rectangle that SURROUNDS a target entity’s bounding box, padded by buff px (default 8) — a highlight box. Move it to another target with surround(box, target). Default colour gold |
surround(box: id or tag, target: id or tag, [dur: number], [ease: easing]) | animation verb | move a highlight box (framebox/rect) to surround a NEW target, gliding + resizing to fit it (Manim ReplacementTransform on surrounding rectangles) |
line(id: new id, from: (x, y), to: (x, y)) | constructor / setup | a line segment; from/to take a (x,y) point OR AN ENTITY NAME (see arrow) |
link(id: new id, from: id or tag, to: id or tag, [bend: number]) | constructor / setup | a straight or bent edge that follows two moving entities |
arrow(id: new id, from: (x, y), to: (x, y), [bend: number]) | constructor / setup | an arrow (optional bend bows it into a curved arc — signed, so the sign picks the side). from/to are a (x,y) point OR AN ENTITY NAME, so an arrow can be aimed at something whose position was computed rather than typed: an atom or bond in a structure (arrow(a, nuc.a0, sub.a1, 62) is a mechanism curly arrow), a graph node, a matrix cell. A path-shaped entity (a bond, a plotted curve) resolves to the centre of its bounds, which is the point a reader means |
polygon(…) | constructor / setup | a filled polygon: polygon(id, (x1,y1), (x2,y2), (x3,y3), …, [color]) |
dot(id: new id, at: (x, y), [r: number]) | constructor / setup | a small filled dot |
cloud(…) | constructor / setup | a point field: N points placed by closed-form formulas of index i and time t |
shader(…) | constructor / setup | a per-pixel colour field: r/g/b (or c) as closed-form formulas of pixel coords u/v and time t |
raymarch(…) | constructor / setup | a 3D SDF scene, ray-marched per pixel: let d = signed distance to the scene at (x,y,z,t) |
glsl(id: new id, source: string) | constructor / setup | run a raw GLSL fragment shader (Shadertoy mainImage, iTime/iResolution) at full resolution |
particles(id: new id, container: id or tag, count: number, [radius: number], [seed: number], [layout: string]) | constructor / setup | deterministic small dots inside a circle or rectangle; optional random/grid/ring layout |
text(id: new id, at: (x, y), text: string, [size: number]) | constructor / setup | a text label; optional trailing point size (default 28) |
label(…) | constructor / setup | a label pinned to an entity |
caption(id: new id, words: string, at: (x, y), [size: number], [color: colour]) | constructor / setup | a row of caption words (karaoke/pop) |
support(id: new id, center: (x, y), [len: number], [dir: string]) | constructor / setup | a hatched fixed support (wall/ceiling/floor) for mechanics diagrams; dir = open side |
counter(id: new id, at: (x, y), value: number, [decimals: number], [prefix: string], [suffix: string]) | constructor / setup | a live numeric readout |
parameter(id: new id, at: (x, y), initial: number, min: number, max: number, [label: string], [decimals: number]) | constructor / setup | a visible bounded value for a smooth multi-visual journey |
bind(parameter: id or tag, target: id or tag, property: new id, formula_or_from: string, [to: number], [formula_component_3: string]) | constructor / setup | connect a parameter to a property, plot/field formula, or stable generated 3-D family |
cursor(id: id or tag) | constructor / setup | give a text entity a typewriter cursor |
sticky(id: id or tag) | constructor / setup | pin an entity to the screen so it stays fixed through cam/zoom (HUD overlay) |
morph(…) | constructor / setup | sampled-point shape morph a->b |
invertpath(id: new id, source: id or tag, center: id or tag, radius: number, [samples: number]) | constructor / setup | live whole-outline inversion about a named center |
reflectpath(id: new id, source: id or tag, mirror: id or tag, [samples: number]) | constructor / setup | live whole-outline reflection across a named line |
copy(new: new id, src: id or tag) | constructor / setup | duplicate an entity |
color(id: id or tag, color: colour) | constructor / setup | set fill/stroke colour |
outline(id: id or tag, color: colour) | constructor / setup | set outline colour |
size(id: id or tag, size: number) | constructor / setup | set text size |
wrap(id: id or tag, width: number) | constructor / setup | wrap a text/caption/$…$ label to a width (px), breaking at word boundaries; inline math stays atomic. Text that would leave the canvas wraps to the room it has WITHOUT this - nothing is silently clipped - so wrap is for choosing a narrower column than the frame allows |
align(id: id or tag, edge: id or tag) | constructor / setup | which edge of a text block is pinned to its point: left, center (the default) or right. Applies to a wrapped block, to hand-broken \n lines, and to an equation image; a right-aligned label keeps its right edge as the text changes length, which is what a column of numbers wants |
leading(id: id or tag, factor: number) | constructor / setup | line height as a multiple of the font size (default 1.4). Tighten it for a stacked column or a title that should read as one block, open it out for a paragraph |
vertical(id: id or tag, [leading: number]) | constructor / setup | stack a text label’s glyphs into a column, each one UPRIGHT - the poster/spine reading. Not the same as rot(id, 90), which turns the whole line on its side and takes the letters with it. Optional line height (default 1.0, tighter than prose because single capitals otherwise read as a list). Spaces become gaps in the column, combining marks stay on their letter, and applying it twice changes nothing. Inline math is refused - a formula cannot be stacked letter by letter |
stroke(id: id or tag, width: number) | constructor / setup | set stroke width |
dashed(id: id or tag, [dash: number], [gap: number]) | constructor / setup | use a repeating dash/gap stroke on a path-like entity |
gradient(…) | constructor / setup | multi-stop gradient on the primary paint: gradient(id, c1, c2, …, [mode]) — along a path stroke by arc length, a linear (angle°) / radial fill, or a computed “speed”/“curvature” quantity on strokes |
glow(id: id or tag, amount: number) | constructor / setup | set neon glow amount |
plate(id: id or tag, [opacity: number]) | constructor / setup | give TEXT a legibility backing: a theme-aware (template background) chip sized to the glyphs so a caption/title stays readable over a busy shader or field — plate(id, [opacity]); default opacity 0.55 |
opacity(id: id or tag, value: number, [layer: id or tag]) | constructor / setup | set opacity 0..1. Optional trailing fill / stroke word sets just THAT layer’s opacity, independent of the other — a translucent interior under a crisp outline (a shape carries its own fill and stroke opacity, like set_fill/set_stroke). Omit the word → whole entity |
hue(id: id or tag, degrees: number, [s: number], [l: number]) | constructor / setup | drive colour by an HSL hue |
rot(id: id or tag, degrees: number) | constructor / setup | set rotation (degrees) |
z(id: id or tag, z: number) | constructor / setup | set draw order |
clip(id: id or tag, region: id or tag) | constructor / setup | clip an entity (or tagged group) to another entity’s rectangular bounds — reveal-through-a-window / crop-to-panel / region-wipe; the scissor follows the region each frame |
mask(id: id or tag, region: id or tag) | constructor / setup | clip an entity (or tagged group) to another entity’s ARBITRARY shape (circle, polygon, star, an imported svg outline) — reveal-through-any-shape / lens / porthole / spotlight; masks any content and follows the region each frame |
tag(id: id or tag, tag: new id) | constructor / setup | add a group tag |
bold(id: id or tag) | constructor / setup | use the bold mono font |
display(id: id or tag) | constructor / setup | mark visible |
hidden(id: id or tag, [from: id or tag]) | constructor / setup | start hidden (opacity 0). Optional from arms a GROW-IN reveal for show (scale 0 at the anchor -> home): center | up/down/left/right (a bbox edge) | tail (an arrow’s start) | a point (x,y). The reveal STYLE lives on hidden, exactly like untraced arms draw |
filled(id: id or tag) | constructor / setup | SOLID posture: fill on, outline off — the default for circle/rect/polygon. Reverts a hollow shape |
outlined(id: id or tag) | constructor / setup | HOLLOW posture: outline on, fill off — a ring/frame you can draw-on (untraced+draw) and give a translucent fill via opacity(id, a, fill). The stroke-first look |
untraced(id: id or tag) | constructor / setup | start undrawn, ready for draw-on |
union(…) | constructor / setup | boolean union of two shapes |
intersect(…) | constructor / setup | boolean intersection |
intersection(…) | constructor / setup | boolean intersection |
difference(…) | constructor / setup | boolean difference a-b |
subtract(…) | constructor / setup | boolean difference a-b |
exclusion(…) | constructor / setup | boolean symmetric difference |
xor(…) | constructor / setup | boolean symmetric difference |
brace(id: new id, p1: (x, y), p2: (x, y), [depth: number], [direction: id or tag]) | constructor / setup | a curly brace between two points; optional depth (amplitude) and direction (“up”/“down”/“left”/“right”) to set which way it bulges — either may be given alone, e.g. brace(b,(x1,y1),(x2,y2),“down”) or brace(b,p1,p2,30,“down”) |
bracelabel(…) | constructor / setup | a brace with a label |
bracetext(…) | constructor / setup | a brace’s text label |
draw(id: id or tag, [dur: number], [ease: easing]) | animation verb | trace a stroke on |
flow(path: id or tag, [dur: number], [direction: id or tag], [mode: id or tag]) | animation verb | send a directional luminous pulse or finite continuous stream over a path |
erase(id: id or tag, [dur: number], [ease: easing]) | animation verb | reverse of draw |
show(id: id or tag, [dur: number], [ease: easing]) | animation verb | reveal an entity: a fade-in, OR a GROW-IN if it was armed with hidden(id, from) (scale 0 at the anchor -> home). One reveal verb; the style is picked by how you hid it |
fade(id: id or tag, [dur: number]) | animation verb | fade out |
flash(id: id or tag, [color: colour], [dur: number], [ease: easing]) | animation verb | flash a colour, then restore |
recolor(id: id or tag, color: colour, [dur: number]) | animation verb | permanently change colour |
pulse(id: id or tag, [dur: number]) | animation verb | grow-and-settle attention pulse |
oscillate(id: id or tag, prop: new id, period: number, amp: number, [phase: number], [dur: number]) | animation verb | continuously oscillate a property sinusoidally — prop is size|opacity|x|y|hue; period seconds/cycle; amp is a scale delta (size), opacity delta, pixels (x/y) or degrees (hue); phase in cycles (0–1) — give looped siblings a per-index phase for a travelling wave; runs for dur (default 6s). Works on a tag. |
breathe(id: id or tag, period: number, amp: number, [phase: number], [dur: number]) | animation verb | size oscillation (alias for oscillate with prop=size): the entity grows and shrinks; give looped siblings a per-index phase for a breathing wave |
shake(id: id or tag, [dur: number]) | animation verb | horizontal shake (error gesture) |
spin(id: id or tag, [degrees: number], [dur: number]) | animation verb | spin about the centre |
move(id: id or tag, to: (x, y), [dur: number], [ease: easing]) | animation verb | move to an absolute point |
wander(particles: id or tag, [dur: number]) | animation verb | gently move a particle group inside its source container |
burst(particles: id or tag, [dur: number]) | animation verb | explode a particle group outward from its container centre, decelerating and fading — the impulse/impact/confetti/ignition beat |
arrange(particles: id or tag, container: id or tag, [layout: string], [dur: number], [ease: easing]) | state-changing verb | move persistent particles into a deterministic random, grid, or ring layout; random uses stable organic curved routes |
travel(entity: id or tag, path: id or tag, [dur: number], [ease: easing]) | state-changing verb | move one persistent entity once along an existing path and stop at its endpoint |
attach(child: id or tag, target: id or tag, [offset: (x, y)]) | state-changing verb | keep an entity pinned to another entity plus an optional offset; use target none to release |
become(source: id or tag, target: id or tag, [dur: number], [ease: easing]) | state-changing verb | transform one entity into a declared visual blueprint while retaining the source id |
turn(id_or_tag: id or tag, pivot: (x, y), degrees: number, [dur: number], [ease: easing]) | state-changing verb | rotate one entity or tagged arrangement rigidly around a shared pivot |
disintegrate(entity: id or tag, [dur: number]) | state-changing verb | dissolve an entity’s actual silhouette into deterministic seekable particles |
shift(id: id or tag, by: (x, y), [dur: number], [ease: easing]) | animation verb | move by a delta |
slidex(id: id or tag, x: number, [dur: number], [ease: easing]) | animation verb | slide to an absolute x, keeping y where it is |
slidey(id: id or tag, y: number, [dur: number], [ease: easing]) | animation verb | slide to an absolute y, keeping x where it is |
dock(group: id or tag, member: id or tag, target: (x, y), [dur: number], [ease: easing]) | animation verb | shift a group rigidly so one member lands on a target point or entity |
groupscale(group: id or tag, factor: number, [dur: number], [ease: easing]) | animation verb | scale a group about its collective centre (not each part about its own) |
sliders(id: new id, n: number, center: (x, y), [width: number], [height: number], [color: colour]) | constructor / setup | a rack of N vertical coordinate dials, each a value in [-1,1] (a point in N-D space as N dials); setsliders drives them and shows the running sum of squares |
setsliders(sliders: id or tag, values: string, [dur: number], [ease: easing]) | animation verb | animate every dial of a sliders rack to its value in [-1,1] (one value per dial) and update the sum-of-squares readout |
deform(id: id or tag, u: string, v: string, [dur: number], [ease: easing]) | state-changing verb | continuous homotopy: over dur, animate t:0->1 and remap an entity’s outline points by (u(x,y,t), v(x,y,t)) each frame — the animated twin of warp. Write t=0 as the identity (u=x, v=y). e.g. a wave: v = y + ampsin(xk + t)sin(pit) |
wiggle(id: id or tag, [dur: number]) | animation verb | an attention WIGGLE: a there-and-back scale pulse + oscillating rotation, returning to base. The rotate+scale cousin of shake (which is translation only) |
blink(id: id or tag, [dur: number]) | animation verb | blink an entity out-and-in twice over dur to draw the eye |
circumscribe(id: id or tag, [color: colour], [dur: number]) | state-changing verb | draw a temporary outlined rectangle around an entity or group, then fade it — a transient surround to call attention. Creates {id}.circ |
spotlight(id: id or tag, [dur: number]) | state-changing verb | a translucent disc converges onto an entity then fades — a converging spotlight that draws focus to it. Creates {id}.spot |
passflash(id: id or tag, [color: colour], [dur: number]) | state-changing verb | a luminous sliver travels along an entity’s outline once, then vanishes (closed shapes: the light circles the whole outline). Creates {id}.pass |
savestate(id: id or tag) | constructor / setup | snapshot an entity’s transform (pos/scale/rotation/colour/opacity); restore(id, ...) later animates it back to how it started |
spiralin(group: id or tag, [dur: number]) | state-changing verb | reveal a group’s members by spiralling them in from a rotated, shrunken offset while fading up, staggered |
trail(id: id or tag, target: id or tag, [color: colour], [thickness: number]) | constructor / setup | a growing motion trail that records where an entity has been |
loupe(id: new id, source: (x, y), sw: number, sh: number, display: (x, y), mag: number, [frame_color: colour], [panel_color: colour]) | constructor / setup | a magnifier: a source frame swsh over the scene at (sx,sy) + a panel at (dx,dy) that shows that region magnified mag live. Animate {id}.frame (move/scale) and the view follows. Manim’s ZoomedScene |
regions(…) | constructor / setup | fill + count the areas the dividers (chords/segments/edges — each an entity or tag) cut a boundary (polygon/rect/circle) into. Computes the planar arrangement; fills every enclosed face {id}.r0, {id}.r1, … (all tagged {id}) with a cycling palette. The number of {id}.r* parts is the region count. Variadic: regions(id, boundary, d1, d2, …) |
spantree(…) | constructor / setup | a spanning tree (forest) of the graph whose edges are the given entities/tags. Greedy union-find in argument order (list preferred edges first to shape the tree). Tree edges = green overlays {id}.t0.. (tagged {id}); leftover co-tree edges = overlays {id}.c0.. (tagged {id}.co). Works on any drawn graph, including a dual. Variadic: spantree(id, e1, e2, …) |
dual(…) | constructor / setup | the DUAL graph of a planar graph: a node at each enclosed face’s centre + one OUTER node, and a dual edge across each primal edge joining the two faces it separates (boundary edge -> outer node). Nodes = dots {id}.n0.. (tagged {id}.nodes); dual edges = lines tagged {id}, so spantree(dt, id) gives the dual’s spanning tree (F-1 edges). Variadic: dual(id, boundary, d1, d2, …) |
grow(id: id or tag, to: (x, y), [dur: number], [ease: easing]) | animation verb | animate a line/arrow endpoint |
scale(id: id or tag, factor: number, [dur: number]) | animation verb | animate scale to a factor |
rotate(id: id or tag, degrees: number, [dur: number], [ease: easing]) | animation verb | animate rotation |
say(id: id or tag, text: string, [dur: number], [ease: easing]) | animation verb | crossfade text to new content |
speak(…) | animation verb | narrate with TTS; service from voice(“gtts”|“cartesia”|“elevenlabs”) (default gTTS); speak(“text”) or speak(caption, “text”); duration is the voice length |
cue(sound: id or tag) | animation verb | play a deterministic procedural sound beat: tick, pop, whoosh, or chime |
rewrite(equation: id or tag, latex: string, [dur: number], [ease: easing]) | state-changing verb | smoothly transform one authored LaTeX equation state into the next while matching unchanged visual parts; this animates supplied math and does not solve it |
type(id: id or tag, [dur: number]) | animation verb | typewriter reveal |
to(id: id or tag, prop: new id, value: number, [dur: number], [ease: easing]) | animation verb | animate any property to a value |
set(id: id or tag, prop: new id, value: number, [dur: number], [ease: easing]) | animation verb | alias of to |
transform(id: id or tag, origin: (x, y), a: number, b: number, c: number, d: number, [dur: number], [ease: easing]) | animation verb | apply a 2x2 matrix (ApplyMatrix) |
swap(a: id or tag, b: id or tag, [dur: number]) | state-changing verb | swap two entities, or array slots i,j |
cycle(…) | state-changing verb | move entities cyclically into one another’s positions along an optional arc |
cam(to: (x, y), [dur: number], [ease: easing]) | animation verb | pan the camera to a point |
zoom(factor: number, [dur: number], [ease: easing]) | animation verb | zoom the camera |
followshot(target: id or tag, [offset: (x, y)]) | state-changing verb | a moving-camera FOLLOW SHOT: the 2D camera tracks an entity, centred on it every frame (+ optional screen offset). Pair with zoom; none releases it (pan/zoom back to restore) |
karaoke(…) | animation verb | highlight caption words in sequence |
wordpop(…) | animation verb | pop caption words in one at a time |
sweep(id: new id, template: id or tag, xparam: new id, xrange: (x, y), yparam: new id, yrange: (x, y), [center: (x, y)], [cols: number], [rows: number], [cellw: number], [cellh: number], [overlays: number], [fit: number]) | constructor / setup | a small-multiples GRID of anything, over any two of its parameters. template is an id you declared normally; each cell rebuilds it with xparam/yparam stepped across their ranges (parameter names come from the builtin’s own signature). Kit-agnostic: it re-invokes the constructor, so it works for any kit, sims or plain shapes. Cells are ordinary entities tagged {id} + {id}.cells; run(id) plays every cell, run({id}.c0x0) just one. Cells are BARE by default (a sim’s {id}.overlays chrome is stripped from the grid) - pass overlays=1 to keep it. fit 0-1 = how much of the MOTION must fit in a cell (1 = never leaves the cell, 0 = fit the resting pose and let a big swing cross the edge) |
Generative
| API | Kind | What it does |
|---|---|---|
lsystem(id: new id, center: (x, y), size: number, axiom: string, rules: string, [options: string]) | constructor / setup | a fitted, batched curve L-system with turtle turns, continuous draw-on, and optional closed concave fill |
repeat(id: new id, motif: id or tag, options: string) | constructor / setup | repeat one entity or tagged motif as a stable hex, grid, or radial arrangement |
ifs2(id: new id, center: (x, y), size: (x, y), count: number, seed: number, rules: string, [options: string]) | constructor / setup | a deterministic batched affine iterated-function system |
mandelbrot(id: new id, center: (x, y), size: (x, y), xrange: (x, y), yrange: (x, y), iterations: number, [columns: number]) | constructor / setup | a batched escape-time Mandelbrot field |
polarpath(id: new id, center: (x, y), scale: number, formula: string, range: (x, y), [samples: number], [closed: number]) | constructor / setup | a sampled polar formula path |
hull2(id: new id, cloud: id or tag, [depth: number], [pivot: number]) | constructor / setup | an onion-depth hull derived from a batched point cloud |
3D
| API | Kind | What it does |
|---|---|---|
cloud3(…) | constructor / setup | a 3D point field: N points placed by closed-form x/y/z formulas of index i and time t |
camera3(eye: (x, y, z), target: (x, y, z), [fov: number], [projection: id or tag], [panel_center: (x, y)], [panel_w: number], [panel_h: number]) | constructor / setup | a Z-up orbit camera. Optionally render the 3D scene into a canvas-space VIEWPORT panel — pass (cx,cy), w, h after the projection and the 3D renders into that bordered sub-rect (a 3D inset in a 2D layout) instead of the whole canvas, using the panel’s own aspect. |
point3(id: new id, at: (x, y, z), [radius: number]) | constructor / setup | a point in 3D space |
line3(id: new id, from: (x, y, z), to: (x, y, z)) | constructor / setup | a 3D line segment |
arrow3(id: new id, from: (x, y, z), to: (x, y, z)) | constructor / setup | a 3D vector arrow |
cross3(id: new id, origin: (x, y, z), v: (x, y, z), w: (x, y, z), [color: colour]) | constructor / setup | the 3D CROSS PRODUCT v x w shown geometrically: the two vectors, the parallelogram they span, and p = v x w perpendicular to both with length = that parallelogram’s AREA (the geometric meaning behind the determinant formula). Vectors {id}.v (green), {id}.w (red), {id}.p (the cross, gold); parallelogram far edges {id}.e1/{id}.e2. All tagged {id}. Orbit with camera3. |
cube3(id: new id, center: (x, y, z), size: (x, y, z)) | constructor / setup | a rotatable 3D cuboid |
linmap3(id: new id, center: (x, y, z), a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, [color: colour]) | constructor / setup | a 3x3 matrix deforming the unit cube; volume = determinant |
eigen3(id: new id, center: (x, y, z), a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, [color: colour]) | constructor / setup | the real eigenvector directions of a 3x3 matrix (invariant lines) |
sphere3(id: new id, center: (x, y, z), radius: number) | constructor / setup | a 3D sphere |
collection3(id: new id, center: (x, y, z), count: number, spread: (x, y, z), seed: number, [radius: number]) | constructor / setup | renderer-batched repeated 3D points with stable indexed children |
collection3data(id: new id, center: (x, y, z), points: string, [radius: number]) | constructor / setup | renderer-batched explicit 3D coordinates with stable indexed children |
links3(id: new id, collection: id or tag, mode: id or tag, [neighbors: number]) | constructor / setup | live relationship mesh over a collection3: chain, nearest, or all |
links3data(id: new id, collection: id or tag, edges: string) | constructor / setup | an explicit arbitrary edge list that follows a 3D collection |
trail3(id: new id, collection: id or tag, [child: number], [thickness: number]) | constructor / setup | live history path for one stable indexed child of a collection3 |
ring3(id: new id, collection: id or tag, child: number, [segments: number]) | constructor / setup | live orbit ring derived from one segment of a collection3 chain |
historyplot(id: new id, collection: id or tag, child: number, component: id or tag, center: (x, y), size: (x, y)) | constructor / setup | screen-space 2D component history derived from one compiled collection3 child |
historyplot3(id: new id, collection: id or tag, child: number, component: id or tag, origin: (x, y, z), size: (x, y)) | constructor / setup | world-space 3D component history derived from one compiled collection3 child |
child3(id: new id, collection: id or tag, index: number, [radius: number]) | constructor / setup | addressable proxy for one stable indexed child of a collection3 |
tree3(id: new id, root: (x, y, z), length: number, angle: number, shrink: number, depth: number, seed: number) | constructor / setup | seeded fractal tree batched as one edge mesh per generation |
vectorfield3(id: new id, center: (x, y, z), half: (x, y, z), u: string, v: string, w: string, [density: number]) | constructor / setup | bounded batched 3D vector field from x/y/z/p component formulas |
grid3(id: new id, center: (x, y, z), half: number, [spacing: number]) | constructor / setup | an XY ground grid |
axes3(id: new id, origin: (x, y, z), length: number, [step: number]) | constructor / setup | three colored coordinate axes with ticks + numbers |
frame3(id: new id, center: (x, y, z), size: (x, y, z), [options: string]) | constructor / setup | bounded scientific axes and selectable XY/XZ/YZ grid planes |
randomwalk3(id: new id, center: (x, y, z), steps: number, [seed: number], [options: string]) | constructor / setup | a deterministic batched 3D axis or turtle walk with optional direction color and depth shading |
hilbert3(id: new id, center: (x, y, z), size: number, order: number, [options: string]) | constructor / setup | a bounded batched 3D Hilbert curve that visits every lattice cell once |
present3(frame: id or tag, mode: id or tag, [dur: number], [ease: easing]) | state-changing verb | restyle one frame3 as textbook linework or spatial color |
move3(id: id or tag, to: (x, y, z), [dur: number], [ease: easing]) | animation verb | move a 3D entity |
shift3(id: id or tag, by: (x, y, z), [dur: number], [ease: easing]) | animation verb | shift a 3D entity |
rotate3(id: id or tag, degrees: (x, y, z), [dur: number], [ease: easing]) | animation verb | rotate to Euler angles |
grow3(id: id or tag, to: (x, y, z), [dur: number], [ease: easing]) | animation verb | retarget a 3D line or arrow |
orbit3(azimuth: number, elevation: number, radius: number, [dur: number], [ease: easing]) | animation verb | orbit the 3D camera |
roll3(degrees: number, [dur: number], [ease: easing]) | animation verb | roll the 3D camera around its viewing direction |
look3(target: (x, y, z), [dur: number], [ease: easing]) | animation verb | move the 3D camera target |
drift3(collection: id or tag, dur: number, [amount: number]) | state-changing verb | compile deterministic bounded ambient motion for a collection3 |
chain3(collection: id or tag, lengths: string, rates: string, dur: number) | state-changing verb | animate a generated dependent chain from per-link lengths and rotation rates |
advect3(collection: id or tag, field: id or tag, dur: number, [rate: number]) | state-changing verb | advect a collection3 through a time-varying vectorfield3 with deterministic RK4 |
followshot3(target: id or tag, [offset: (x, y, z)]) | state-changing verb | make the 3D camera target follow an entity, or release with none |
view3(target: id or tag, view: string, [dur: number], [ease: easing], [margin: number]) | state-changing verb | smoothly frame a 3D entity or tagged group from a named view |
travel3(entity: id or tag, path: id or tag, [dur: number], [ease: easing]) | state-changing verb | move a persistent 3D entity along a line, arrow, or curve |
attach3(child: id or tag, target: id or tag, [offset: (x, y, z)], [mode: id or tag]) | state-changing verb | attach one 3D entity to another, or release it with none |
become3(source: id or tag, blueprint: id or tag, [dur: number], [ease: easing]) | state-changing verb | transform a persistent 3D entity into a blueprint |
turn3(target: id or tag, pivot: (x, y, z), axis: id or tag, degrees: number, [dur: number], [ease: easing]) | state-changing verb | turn a 3D entity or tagged group around a world-space pivot and axis |
pin3(label: id or tag, at: (x, y, z), [offset: (x, y)]) | constructor / setup | glue a 2D label to a 3D point with an optional screen-space offset |
follow3(id: id or tag, target: id or tag, [offset: (x, y, z)]) | constructor / setup | track another 3D entity + offset each frame |
midpoint3(id: new id, a: id or tag, b: id or tag) | constructor / setup | a point at the midpoint of two 3D entities |
link3(id: new id, a: id or tag, b: id or tag, [trim: number]) | constructor / setup | a live 3D edge between two moving entities |
project3(id: new id, source: id or tag, plane: string) | constructor / setup | live orthogonal projection of a 3D entity onto xy, xz, or yz |
projectpath3(id: new id, source: id or tag, plane: id or tag) | constructor / setup | live orthogonal projection of a complete changing 3D path |
contour3(id: new id, surface: id or tag, level: number) | constructor / setup | a level curve on a surface3 height field |
label3(label: id or tag, target: id or tag, [world_height: number]) | constructor / setup | projected label with optional world-space height |
curve3(id: new id, x: string, y: string, z: string, [domain: (x, y)]) | constructor / setup | parametric 3D curve from x(t)/y(t)/z(t) formulas |
trajectory3(id: new id, dx/dt: string, dy/dt: string, dz/dt: string, start: (x, y, z), [steps: number], [dt: number]) | constructor / setup | integrate a 3D ODE (dx/dt, dy/dt, dz/dt) with RK4 — draws strange attractors (Lorenz, …) |
surface3(id: new id, z: string, xdom: (x, y), ydom: (x, y), [res: number]) | constructor / setup | filled, flat-shaded height-field surface z = f(x,y) |
domainsurface(id: new id, formula: string, xdom: (x, y), ydom: (x, y), [res: number], [height: number]) | constructor / setup | DOMAIN COLOURING in 3D — the modulus surface of a complex function f(z): each grid point is z=x+iy, the surface HEIGHT is height·|f(z)| (poles clamped finite) and each vertex’s COLOUR is arg f(z) (phase hue). Orbit with camera3. Write f in z/i (e.g. 1/(z*z+1), (z-1)/(z+1), sin(z)). res = grid resolution (default 48), height = vertical scale (default 1). |
implicit3(id: new id, formula: string, xdom: (x, y), ydom: (x, y), zdom: (x, y), [level: number], [resolution: number]) | constructor / setup | an isosurface extracted from a bounded 3D scalar field |
lsystem3(id: new id, origin: (x, y, z), step: number, angle: number, iterations: number, axiom: string, rules: string, [max_symbols: number]) | constructor / setup | a bounded deterministic branching 3D turtle grammar |
heightmap3(id: new id, grid: id or tag, z: string, [size: number]) | constructor / setup | lift a 2D grid into a 3D terrain mesh: z(x,y,h), h=cell value |
gradient3(id: new id, surface: new id, x: number, y: number, [color: colour]) | constructor / setup | steepest-ascent arrow on a surface3 at (x,y) |
slice3(id: new id, surface: new id, axis: id or tag, value: number, [at: number], [color: colour]) | constructor / setup | the geometric partial derivative: cut a surface3 at a constant coordinate (axis = x or y) and draw the cross-section curve riding the surface, whose steepness is the partial along the free axis; with at, also draw the tangent whose slope is that partial at that point |
descend3(id: new id, surface: new id, x: number, y: number, [rate: number], [steps: number], [color: colour]) | constructor / setup | gradient DESCENT on a surface3: from a start (x,y), step against the gradient (rate, steps) and trace the path the ball rolls downhill to a minimum; draw the path to roll it on, {id}.ball marks the start |
tangentplane3(id: new id, surface: new id, x: number, y: number, [color: colour]) | constructor / setup | the plane tangent to a surface3 at (x,y) |
volume3(id: new id, surface: new id, [res: number], [color: colour]) | constructor / setup | the volume under a surface3 as a grid of columns |
param3(id: new id, x: string, y: string, z: string, udom: (x, y), vdom: (x, y), [res: number]) | constructor / setup | parametric surface x(u,v),y(u,v),z(u,v) — tori, shells, Möbius |
pieces3(id: new id, source: new id, cols: number, [rows: number], [inset: number]) | constructor / setup | cut a param3/surface3 into addressable pieces ({id}.r{i}c{j}, tags {id}.row{i}/.col{j}); they follow a bound source as it deforms |
prism3(id: new id, center: (x, y, z), sides: number, radius: number, height: number) | constructor / setup | regular n-gon prism (many sides ≈ cylinder) |
pyramid3(id: new id, center: (x, y, z), sides: number, radius: number, height: number) | constructor / setup | regular n-gon pyramid (many sides ≈ cone) |
revolve3(id: new id, center: (x, y, z), profile: string, tdom: (x, y), [sides: number]) | constructor / setup | solid of revolution from a radius profile r(t) |
extrude3(id: new id, source: id or tag, height: number, [center: (x, y, z)]) | constructor / setup | extrude a 2D shape or boolean region into a solid prism (CSG) |
morph3(a: id or tag, b: id or tag, [spin: number]) | constructor / setup | set 3D entity a up to morph into b’s shape (animate with to(a, morph, 1, dur)) |
thick(id: id or tag, radius: number) | constructor / setup | render a 3D curve3/line3/arrow3 as a tube of the given world radius |
finish3(id: id or tag, spec: string) | constructor / setup | bounded 3D finish: shading (flat|smooth), material (matte|metal|glass), texture (solid|checker|stripes), scale, mesh, wire (0..1 — render any solid as a bright triangulated WIREFRAME with no filled faces; sphere/cube/prism/pyramid/extrude/surface), depth, shadow |
model3(id: new id, path: string, center: (x, y, z), [scale: number]) | constructor / setup | load deterministic geometry from a bundled asset: URI or safety-limited OBJ path |
assembly3(id: new id, path: string, center: (x, y, z), [scale: number]) | constructor / setup | load grouped OBJ geometry as addressable id.group entities sharing one id tag |
tube3(id: new id, path: id or tag, radius: string, [sides: number]) | constructor / setup | variable-radius tube around a line3, arrow3, or curve3 |
Math
| API | Kind | What it does |
|---|---|---|
colorwheel(id: new id, center: (x, y), radius: number) | constructor / setup | the LEGEND for domain colouring: a hue disk of radius px where angle = phase (arg) and the centre darkens (small |z|) toward a bright rim — the key for domaincolor/domainsurface. |
warp(id: new id, center: (x, y), unit: number, formula: string, [range: number], [res: number]) | constructor / setup | a coordinate grid over the complex plane that DEFORMS under a complex function z->f(z): each grid line is sampled from f (so it bends into a curve). Starts as the identity grid and morphs to the warped image when you animate it — to(id, morph, 1, dur) (broadcasts over the whole grid). Write f in z/i (e.g. z*z, 1/z, (z-1)/(z+1), sin(z)). The complex/non-linear twin of linmap and the deforming-grid companion to domaincolor. range = half-width in complex units (default 3, integer grid lines); res = samples per line (curve smoothness, default 28). Lines tagged {id}. |
squish(id: new id, center: (x, y), unit: number, a: number, b: number, [span: number]) | constructor / setup | the DOT PRODUCT as a transformation (DUALITY): the whole plane collapses onto a NUMBER LINE under the 1x2 map [a b], sending (x,y) -> ax + by. Animate with to(id, morph, 1, dur) — the grid squishes onto the line, i-hat lands on a, j-hat on b. The gold DUAL VECTOR (a,b) ({id}.dual) is the 2D vector whose dot product IS this map (the duality). The rank-reducing sibling of gridmap; output line + ticks are {id}.line. |
domaincolor(id: new id, center: (x, y), w: number, h: number, formula: string, [range: number]) | constructor / setup | domain colouring of a complex function f(z) over a w×h plane region: each pixel is z=x+iy, hue = arg f(z), brightness from |f(z)| (zeros dark, poles white). Write f in terms of the complex variable z and the imaginary unit i (e.g. z^2+1, 1/(z*z+1), sin(z), (z-1)/(z+1)); constants pi/tau/e and functions sin/cos/tan/sinh/cosh/tanh/exp/ln/sqrt/conj/abs/re/im AND zeta (the Riemann zeta function, valid through the critical strip — zeta(z) shows the pole at s=1 and zeros as dark points). range = half-width in complex units along the real axis (default 4; imaginary extent scales by aspect). |
axes(id: new id, center: (x, y), halfw: number, halfh: number, [unit: number]) | constructor / setup | a coordinate frame (optional tick spacing in px) |
coords(id: new id, origin: (x, y), xrange: (x, y), yrange: (x, y), sx: number, sy: number, [tips: number], [step: number], [numbers: number], [xname: string], [yname: string]) | constructor / setup | a flexible coordinate frame: arbitrary ranges, per-axis scale, optional tips/ticks/auto-numbers, and optional axis names. Origin (0,0) maps to origin; align a plot with the same origin+sx+sy. Use xtick/ytick for custom (non-numeric) tick labels |
xtick(id: new id, coords: new id, value: number, [text: string]) | constructor / setup | a tick + label on a coords frame’s x-axis at an axis VALUE (not a pixel); label is any text — number, word (“Mon”), or symbol (“π/2”) — omit to show the value |
ytick(id: new id, coords: new id, value: number, [text: string]) | constructor / setup | a tick + label on a coords frame’s y-axis at an axis VALUE (see xtick) |
field(id: new id, formula: string) | constructor / setup | define a REUSABLE scalar field — a pure function of x and y: field(name, “expr”). Call it name(a, b) inside ANY formula (plot / surface3 / shader / cloud) and it INLINES, so every view samples the identical expression (author once, view many ways — provably the same field; e.g. plot name(x,0), surface3 name(x,y), a heatmap shader name((u-0.5)*8,(v-0.5)*8)). Use name(x, 0) for a 1-D slice. Body may use only x, y, pi/tau/e, and functions — pass time/extra inputs at the call site. Not an entity; references only earlier-defined fields (no cycles). |
plot(id: new id, center: (x, y), sx: number, sy: number, formula: function or formula, [domain: number]) | constructor / setup | y = f(x) as a curve |
param(id: new id, center: (x, y), sx: number, sy: number, x: string, y: string, [tdomain: number]) | constructor / setup | a PARAMETRIC curve (x(t), y(t)) — the twin of plot for curves that aren’t y=f(x): spirals, roses, Lissajous. Two formulas in the parameter t; tdomain is a (t0,t1) pair or a scalar giving [0,t] (default [0, tau]). sx/sy are px per unit, origin at center. |
polar(id: new id, center: (x, y), sx: number, sy: number, r: string, [tdomain: number]) | constructor / setup | a POLAR curve r = f(t) with t the angle θ; traces (r·cos t, r·sin t). tdomain is a (t0,t1) pair or a scalar [0,t] (default [0, tau]). Roses cos(3t), spirals t, limaçons. |
normal(id: new id, curve: new id, x: number, [len: number]) | constructor / setup | the normal (perpendicular) line to a curve at x |
slope(id: new id, curve: new id, x: number, [offset: (x, y)]) | constructor / setup | a live readout of a curve’s slope at x |
slopetri(id: new id, curve: new id, x: number, [run: number]) | constructor / setup | a slope triangle on a curve at x: a run leg (length run, default 1, labeled {id}.one) + a red rise leg up to the tangent, proving slope = rise/run. Parts {id}.run/.rise/.one share tag {id}, so to(id, x, ...) slides the whole triangle in lock-step. |
area(id: new id, curve: new id, a: number, b: number, [n: number]) | constructor / setup | the filled region under a curve from a to b |
integral(id: new id, curve: new id, a: number, b: number, [at: (x, y)]) | constructor / setup | a live readout of the integral of a curve from a to b |
roots(id: new id, curve: new id, [color: colour]) | constructor / setup | dots where a curve crosses zero |
deriv(id: new id, curve: new id, [color: colour]) | constructor / setup | the derivative f’ of a curve, drawn as its own curve |
accum(id: new id, curve: new id, [a: number], [color: colour]) | constructor / setup | the accumulation function integral(a..x) of a curve |
extrema(id: new id, curve: new id, [color: colour]) | constructor / setup | dots at a curve’s maxima and minima (slope = 0) |
inflections(id: new id, curve: new id, [color: colour]) | constructor / setup | dots where a curve changes concavity (f’’ = 0) |
band(id: new id, top: new id, bottom: new id, [color: colour], [range: (x, y)]) | constructor / setup | the filled region between two curves (optional (a,b) restricts it to that x-slice) |
vline(id: new id, curve: new id, x: number, [color: colour], [style: id or tag]) | constructor / setup | a guide line from the x-axis up to a plotted curve at x; dotted by default (style: dotted/dashed/solid) |
curvedot(id: new id, curve: new id, x: number, [color: colour]) | constructor / setup | a dot that rides a plotted curve at x, always on the graph; slide it with to(id, x, …). Named curvedot and not mark, because mark("name") is the timeline beat marker |
graphlabel(id: new id, curve: new id, latex: string, [x: number], [direction: id or tag], [color: colour]) | constructor / setup | a LaTeX label pinned to a plotted curve at x (default: the curve’s right end), nudged clear of the line in a direction (up/down/left/right/upright/upleft/downright/downleft, default up); colour defaults to the curve’s. Put LaTeX in BACKTICKS |
boxto(id: new id, curve: new id, x: number, [color: colour]) | constructor / setup | a filled rectangle from the origin (0,0) to the point (x, f(x)) on a curve (an inscribed box, area x*f(x)); slide it with to(id, x, …) — on y=k/x the area stays constant |
riemann(id: new id, curve: new id, a: number, b: number, [dx: number], [color: colour]) | constructor / setup | Riemann rectangles under a curve from a to b (bars of width dx, left-endpoint sampled) — the discrete analogue of area; default colour green |
taylor(id: new id, curve: new id, a: number, n: number, [color: colour]) | constructor / setup | the degree-n Taylor polynomial of a curve about a |
limit(id: new id, curve: new id, a: number, [color: colour]) | constructor / setup | visualise lim(x->a) f(x): the value approached, with an approaching dot |
newton(id: new id, curve: new id, x0: number, [steps: number]) | constructor / setup | Newton’s method zig-zag from a starting guess |
spline(…) | constructor / setup | a smooth curve through a list of points |
trajectory(id: new id, dx/dt: string, dy/dt: string, start: (x, y), center: (x, y), scale: number, [steps: number]) | constructor / setup | an ODE path: dx/dt, dy/dt integrated from a start point |
linmap(id: new id, center: (x, y), unit: number, a: number, b: number, c: number, d: number, [span: number]) | constructor / setup | a 2x2 matrix applied to the plane (grid + basis), shown STATICALLY as a before/after (faint identity grid + bright transformed grid). For the ANIMATED version use gridmap. |
gridmap(id: new id, center: (x, y), unit: number, a: number, b: number, c: number, d: number, [span: number], [a0: number], [b0: number], [c0: number], [d0: number]) | constructor / setup | an ANIMATED 2x2 matrix transform of the plane: a coordinate grid + basis i-hat/j-hat that MORPHS from the identity to [[a,b],[c,d]] when you animate it — to(id, morph, 1, dur) sweeps all of space deforming (the iconic ‘matrices transform space’). A faint identity grid {id}.bg stays behind. The animated twin of linmap (static before/after) and the linear sibling of warp (complex). i-hat lands on the first column, j-hat on the second. Optional FROM matrix a0,b0,c0,d0 (default identity): morph BETWEEN two matrices [[a0,b0],[c0,d0]] -> [[a,b],[c,d]] so a second gridmap continues where the first left off (matrix multiplication = composition). |
determinant(id: new id, center: (x, y), unit: number, a: number, b: number, c: number, d: number, [color: colour]) | constructor / setup | the unit square mapped to a parallelogram; area = det |
eigen(id: new id, center: (x, y), unit: number, a: number, b: number, c: number, d: number, [color: colour]) | constructor / setup | a matrix’s real eigenvector directions (with eigenvalues) |
diagonalise(id: new id, center: (x, y), unit: number, a: number, b: number, c: number, d: number, [color: colour]) | constructor / setup | A = P D P^-1: in the eigenbasis the matrix is a pure stretch |
diagonalize(id: new id, center: (x, y), unit: number, a: number, b: number, c: number, d: number, [color: colour]) | constructor / setup | A = P D P^-1: in the eigenbasis the matrix is a pure stretch |
rref(id: new id, data: string, center: (x, y), [cellw: number], [rowh: number]) | constructor / setup | animated Gaussian elimination to reduced row-echelon form |
project(id: new id, center: (x, y), unit: number, b: (x, y), a: (x, y), [color: colour]) | constructor / setup | orthogonal projection of a vector onto a line (subspace) |
leastsquares(id: new id, center: (x, y), unit: number, data: string, [color: colour]) | constructor / setup | the best-fit line through a point cloud (linear regression) |
linsolve(id: new id, center: (x, y), unit: number, a: number, b: number, c: number, d: number, e: number, f: number, [span: number]) | constructor / setup | the row picture of Ax=b: two lines meeting at the solution |
span(id: new id, center: (x, y), unit: number, v: (x, y), [w: (x, y)], [color: colour]) | constructor / setup | the span of one or two vectors (a line, or the whole plane) |
vector(id: new id, origin: (x, y), delta: (x, y), [color: colour]) | constructor / setup | an arrow from an origin |
numberline(…) | constructor / setup | a labelled number line |
numberplane(…) | constructor / setup | a gridded coordinate plane |
plane(…) | constructor / setup | a coordinate plane |
complexplane(…) | constructor / setup | the complex plane |
polarplane(…) | constructor / setup | a polar grid |
matrix(id: new id, data: string, center: (x, y), [colw: number], [rowh: number]) | constructor / setup | a bracketed matrix |
table(id: new id, data: string, center: (x, y), [colw: number], [rowh: number], [cols: string], [rows: string]) | constructor / setup | a ruled table |
mathtable(…) | constructor / setup | a table of math expressions |
integertable(…) | constructor / setup | a table of integers |
decimaltable(…) | constructor / setup | a table of decimals |
arc(…) | constructor / setup | a circular arc |
sector(…) | constructor / setup | a pie sector |
annulus(…) | constructor / setup | a ring / annular sector |
pie(…) | constructor / setup | a pie chart |
arrowfield(…) | constructor / setup | a named or formula-authored bounded vector field |
vectorfield(…) | constructor / setup | a named or formula-authored bounded vector field |
tangent(…) | constructor / setup | tangent line to a curve at x (or tangent points to a circle) |
Process
| API | Kind | What it does |
|---|---|---|
livehistogram(id: new id, center: (x, y), min: number, max: number, bins: number, [width: number], [height: number], [color: colour]) | constructor / setup | an initially empty histogram driven by real measurements from a deterministic collection process |
stream(particles: id or tag, path: id or tag, dur: number, [spread: number], [ease: easing]) | state-changing verb | progressively send a persistent particle collection along a path and retain truthful arrival/speed measurements |
emit(particles: id or tag, path: id or tag, profile: id or tag, dur: number, [spread: number], [ease: easing]) | state-changing verb | release a persistent particle collection with deterministic individual speeds sampled from a named profile |
advect(target: id or tag, field: id or tag, duration: number, [rate: number]) | state-changing verb | carry a particles collection OR a whole cloud through a bounded vector field with deterministic seekable RK4 integration. A particles collection keeps every child individually addressable (capped at 500 by particles); a cloud is batched, so its paths are precompiled and the whole swarm — tens of thousands of points — rides one track. Cloud seeds come from the cloud’s own formulas, so the flow starts from the shape you drew. Either way a particle reaching the field boundary stops there |
branch(particles: id or tag, paths: id or tag, dur: number, [ease: easing]) | state-changing verb | send every persistent item through one seeded route in a tagged directed acyclic path network |
collect(histogram: id or tag, particles: id or tag, measurement: id or tag, [dur: number], [ease: easing]) | state-changing verb | catch processed particles in the live-histogram bins selected by their real speed, arrival, outcome, or step measurement |
observe(target: id or tag, particles: id or tag, measurement: id or tag) | animation verb | drive a counter or live histogram from measurements produced by the latest stream, emit, or branch process |
Charts
| API | Kind | What it does |
|---|---|---|
racechart(id: new id, layout: string, periods: string, [title: string]) | constructor / setup | declare an animated race chart: layout “bar” (v1), a period-label list that ticks as it plays, and an optional title; feed rows with racedata, then play with race |
racedata(parent: new id, block: string) | constructor / setup | paste the data for a race chart — one row per entity: label [icon] v0 v1 …; rows split on newline or ;, cells on comma/tab/whitespace; the optional icon is an SVG shorthand (us → a flag) or a full asset path |
raceseries(parent: new id, label: string, icon_or_values: string, [values: string]) | constructor / setup | add ONE entity to a race chart (the per-series sibling of racedata, for the computed/loop path): raceseries(parent, "label", ["icon"], "v0 v1 …") |
raceline(parent: new id, [label: string], [values: string]) | constructor / setup | add a companion LINE across the top of a bar race — a running total (auto-summed from the bars) or an explicit tracked metric: raceline(parent, ["label"], ["v0 v1 …"]) |
racepanel(parent: new id) | constructor / setup | add a multi-line HISTORY panel at the bottom of a bar race — every series drawn as a line up to a moving time-cursor, synced to the race (the classic bar+line combo): racepanel(parent) |
race(parent: new id, [dur: number], [ease: new id]) | animation verb | play the whole race: interpolate each series between periods, re-rank, slide bars to their new slots (the reorder IS the race), rescale to the running max, and tick the period + value counters |
Systems
| API | Kind | What it does |
|---|---|---|
architecture(id: new id, [center: (x, y)], [width: number], [height: number]) | constructor / setup | an automatically laid-out diagram canvas; geometry is optional and auto-fits the canvas when omitted |
flowchart(id: new id, [direction: id or tag], [max_nodes: number]) | constructor / setup | an edge-ranked flowchart canvas (Mermaid graph TD/LR); with no direction it auto-orients TD/LR to fit and re-decides as nodes are added; reuses the architecture layout/ports/scale-to-fit. Optional max_nodes is the readability split limit (default 6 top-down, 12 left-right) — over it, check warns to split into linked sub-flows |
c4(id: new id, [level: id or tag]) | constructor / setup | a C4-model diagram canvas (Context/Container/Component); inside it node kinds read as C4 elements (person/system/container/component/external) with name/[type: tech]/description |
node(id: new id, parent: id or tag, kind: string, label: string, [description: string], [technology: string]) | constructor / setup | a component inside an architecture/cluster/c4/flowchart; kind is a provider icon, native archetype, flowchart shape, or C4 element. In a c4 diagram the extra args are the box’s description and technology |
cluster(id: new id, parent: id or tag, label: string, [legacy_members: string]) | constructor / setup | a labelled responsive ownership group; children name it as their parent |
connect(id: new id, from: id or tag, to: id or tag, [routing: routing], [from_port: port], [to_port: port]) | constructor / setup | a dashed directed possibility with an optional bend or port-aware orthogonal route |
annotate(edge: id or tag, text: string) | constructor / setup | a small caption at a connection’s midpoint (a decision’s yes/no, or any edge annotation) |
message(id: new id, source: id or tag, label: string) | constructor / setup | a persistent generic message starting at one system node |
request(id: new id, source: id or tag, label: string) | constructor / setup | a persistent labelled request starting at one system node |
route(message: id or tag, connection: id or tag, [duration: number], [ease: easing]) | state-changing verb | move one persistent message through a continuous named connection and illuminate its selected lane |
hotpath(message: id or tag, [duration: number], [seed: number]) | state-changing verb | move one persistent message end-to-end over a seeded valid path, choosing one lane at each fan-out |
Geometry
| API | Kind | What it does |
|---|---|---|
point(id: new id, at: (x, y), [label: string]) | constructor / setup | a labelled point |
segment(id: new id, a: id or tag, b: id or tag) | constructor / setup | segment between two points |
midpoint(id: new id, a: id or tag, b: id or tag) | constructor / setup | midpoint of two points |
centroid(…) | constructor / setup | centroid of a triangle |
circumcenter(…) | constructor / setup | circumcentre |
incenter(…) | constructor / setup | incentre |
orthocenter(…) | constructor / setup | orthocentre |
foot(…) | constructor / setup | foot of a perpendicular |
meet(…) | constructor / setup | line-line intersection |
linecircle(…) | constructor / setup | line-circle intersection |
circlecircle(…) | constructor / setup | circle-circle intersection |
commontangent(id: new id, oA: id or tag, aOn: id or tag, oB: id or tag, bOn: id or tag, [type: string]) | constructor / setup | a common tangent to two circles (each = centre + a point on it): segment {id} between the touch points {id}.a/{id}.b; type external/direct (default) or internal/transverse |
reflect(…) | constructor / setup | reflect a point over a line |
bisector(…) | constructor / setup | angle bisector |
rotpoint(…) | constructor / setup | rotate a point about another |
between(…) | constructor / setup | a point between two others |
anglepoint(…) | constructor / setup | a point at an angle |
circumcircle(…) | constructor / setup | circumscribed circle |
incircle(…) | constructor / setup | inscribed circle |
circle2(…) | constructor / setup | circle from centre + a point on it |
ellipse(…) | constructor / setup | an ellipse |
parabola(…) | constructor / setup | a parabola |
hyperbola(…) | constructor / setup | a hyperbola |
fullline(…) | constructor / setup | an infinite line through two points |
anglemark(…) | constructor / setup | an angle arc mark (optional label rides the bisector) |
rightangle(…) | constructor / setup | a right-angle square mark |
Algorithms and data structures
| API | Kind | What it does |
|---|---|---|
graph(id: new id, verts: string, edges: string, layout: new id, center: (x, y), scale: number, [radius: number]) | constructor / setup | a node/edge graph (weights via a-b:w) |
array(id: new id, vals: string, center: (x, y), [cw: number], [ch: number]) | constructor / setup | a row of value cells in slot boxes |
list(id: new id, vals: string, center: (x, y), [kind: new id], [cw: number], [ch: number]) | constructor / setup | a linked list (singly/doubly/circular) |
stack(id: new id, center: (x, y), [cw: number], [ch: number]) | constructor / setup | a stack (LIFO, grows up) |
queue(id: new id, center: (x, y), [cw: number], [ch: number]) | constructor / setup | a queue (FIFO, grows right) |
hashmap(id: new id, n: number, center: (x, y), [ew: number], [ch: number]) | constructor / setup | n buckets with separate chaining |
pointer(id: new id, arr: id or tag, slot: number, [label: string]) | constructor / setup | an index caret under an array slot |
caret(id: new id, at: (x, y), label: string, [dir: new id]) | constructor / setup | a labelled triangle marker |
compare(arr: id or tag, i: number, j: number, [color: colour]) | animation verb | flash the values in two array slots |
pointat(id: id or tag, arr: id or tag, slot: number, [dur: number]) | animation verb | slide an index pointer to a slot |
push(id: id or tag, value: string, [dur: number]) | state-changing verb | push onto a stack |
pop(id: id or tag, [dur: number]) | state-changing verb | pop the top of a stack |
enqueue(id: id or tag, value: string, [dur: number]) | state-changing verb | enqueue at the back |
dequeue(id: id or tag, [dur: number]) | state-changing verb | dequeue from the front |
insert(id: id or tag, after: number, value: string, [dur: number]) | state-changing verb | splice a node into a list |
remove(id: id or tag, index: number, [dur: number]) | state-changing verb | unlink a list node |
put(id: id or tag, key: string, val: string, [dur: number]) | state-changing verb | hash a key into a bucket + chain |
get(id: id or tag, key: string, [dur: number]) | animation verb | scan a bucket’s chain for a key |
bfs(g: id or tag, start: id or tag) | state-changing verb | breadth-first traversal (queue) |
dfs(g: id or tag, start: id or tag) | state-changing verb | depth-first traversal (stack) |
dijkstra(g: id or tag, start: id or tag) | state-changing verb | single-source shortest paths |
Circuit
| API | Kind | What it does |
|---|---|---|
circuit(id: new id, center: (x, y), netlist: string, [unit: number], [labels: number], [build: number]) | constructor / setup | draw a schematic from a netlist and solve it. One component per line — <type> <x1> <y1> <x2> <y2> [key=value …], ground <x> <y>, # comments — placed on an integer grid, and the TOPOLOGY comes from the geometry: points that coincide (or are joined by a wire) are the same node, so there are no node numbers to write. ground is a REFERENCE, not a component: the solver needs one node to measure from, and with no ground line the kit elects the first source’s negative terminal — so a single loop solves identically either way and the symbol is just clutter. Write one when several sources or loops make it worth saying which node is zero. Known types: wire, ground, resistor, capacitor, inductor, dc-voltage, ac-voltage, dc-current, switch, lamp, diode, led, zener, bjt-npn, bjt-pnp, mosfet-n, mosfet-p, opamp (junctions, transistors and op-amps are solved by Newton–Raphson; is=/n= set a junction, vz= a zener’s breakdown, beta= a bipolar’s gain, vth=/kp= a MOSFET, gain=/rail= an op-amp) — plus DIGITAL: and-gate, or-gate, nand-gate, nor-gate, xor-gate, not-gate, logic-input (state=1), logic-output. A gate’s output is a real 5 V / 0 V node, held for one timestep, so a chain settles a gate-delay at a time. SEQUENTIAL: d-flipflop, sr-flipflop, jk-flipflop — four pins each (the two outputs are the derived pins one and two steps below the second coordinate), and each output is separately driven, so Q and Q̄ are real complementary nodes. SWITCHING AND THRESHOLDS: clock (a square wave, value is its frequency in Hz), push-switch (normally OPEN, closed only while pressed), spdt-switch (three pins: common, then a throw on each of the derived pins, state=1 selects the first), ideal-switch (three pins: a control pin that carries no current, then the two terminals it connects — threshold= sets the control voltage it closes above), fuse (rating= amps; it blows once and stays open, and the symbol shows it), comparator (three pins: V+, V−, output — 5 V when V+ is higher), schmitt (vhi=/vlo= its two switching points, which is what stops it chattering on a noisy crossing), polarized-cap (a capacitor that draws its polarity). MORE DEVICES: jfet-n/jfet-p (idss=/vpinch=) — a DEPLETION transistor, conducting at zero gate voltage and pinched off by a negative one, which is the opposite of a MOSFET; relay (coilr=/pullin=) — FOUR pins, a coil and the contacts it closes, two circuits coupled mechanically rather than electrically; vco (fcenter=/gain= hertz per volt, three pins: control, output, ground) — a square wave whose frequency follows its control pin, integrated as a phase so the frequency can move without the waveform jumping. A THREE-pin part still takes two coordinates — its third pin sits one grid step below the second, and a relay’s fourth sits below its FIRST. COMPOSITES, built out of INTERNAL nodes the geometry never names: darlington-npn/darlington-pnp (beta1=) is genuinely two transistors sharing a base–emitter connection, so it carries both the β₁·β₂ gain and the two-junction 1.2 V base drop; transmission-line (z0=/delay=/sections=, FOUR pins — the input pair are the written points, the output pair hang below them) is a ladder of L–C sections, which is what a DELAY actually is, so a step arrives late rather than instantly (more sections = a cleaner edge; the delay itself is exact either way). Values take SI suffixes (1k, 4.7u, 100n). Parts are {id}.c{k} (and {id}.<name> when a line says name=R1), tagged {id} + {id}.parts + {id}.<type>s; junction dots are {id}.n{node}, tagged {id}.nodes; value text is tagged {id}.labels. unit is px per grid step (default 46); labels 0 hides the values. The schematic DRAWS ITSELF by default, one component at a time in netlist order — the netlist is already the order a teacher would draw it in — and each PREFIX of it is really solved, so nothing flows until the loop closes and then everything does at once. build is the share of run spent drawing (default 0.35): it needs a run(id, dur) to play, and build 0 gives the finished schematic at t=0 instead, which is what a film’s opening frame wants. Value labels, junction dots and probes arrive with the component they belong to. Solved by Modified Nodal Analysis, so KCL and KVL hold by construction |
probe(circuit: new id, at: (x, y), [offset: (x, y)]) | constructor / setup | a live readout on a circuit, in either of two forms. Given a GRID POINT a component touches, it reads that node’s VOLTAGE; given the bareword NAME of a part (name=R1 on its netlist line), it reads the CURRENT through it, in amps scaled to a readable unit. Either way it shows a ring where the measurement is taken and a number beside it, and the number follows the transient when you run the circuit. Optional (dx,dy) moves the label. Entities are {id}.v{node} (or {id}.i{name}) + .at, tagged {id} + {id}.probes |
current(circuit: new id, [speed: number], [shape: id or tag], [color: colour], [size: number]) | constructor / setup | how a circuit’s current READS — presentation only, the solver never sees it. speed multiplies every branch by the same factor (1 = the circuit’s own pace, 2 = twice as fast), so a branch carrying twice the current still moves twice as fast: the PROPORTIONALITY is what carries the meaning and scaling it uniformly cannot lie. shape is circle (default), square or diamond; color any palette name or #rrggbb (default gold, the conventional-current colour — cyan is the usual choice for electron flow); size is the radius in px (default 3). Write it after the circuit it restyles |
cut(circuit: new id, part: id or tag, [dur: number]) | state-changing verb | TAKE A COMPONENT OUT of a circuit — and mean it. The circuit that is left is re-analysed and re-solved from scratch, so breaking a loop really does stop the current everywhere, and a meter on another branch falls to zero on its own. Address the part by its netlist name=, or by position as c0, c1, … The component fades out (with its value label), every charge dot goes with it, and each probe moves to what it now reads; the next run(id, dur) replays the circuit that remains. Cutting one wire of a loop leaves the gap where it was, which is the whole demonstration |
reconnect(circuit: new id, part: id or tag, [dur: number]) | state-changing verb | put a cut component back: the same mechanism in the other direction, so the circuit is solved again with it in place and the next run shows the current returning. (Named reconnect, not restore, because restore is already the general savestate rollback verb) |
scope(circuit: new id, at: (x, y), center: (x, y), [w: number], [h: number]) | constructor / setup | the WAVEFORM of a circuit node or part: a trace of the pre-simulated transient with a sweep line that keeps step with run. at is a grid point (that node’s voltage) or the bareword name of a part (its current), exactly like probe. Both axes are labelled with their real extent — the peak the trace reaches and the time window the circuit chose for itself — because a curve without a scale is decoration. Parts are {id}.scope.<what>.curve / .frame / .sweep / .vscale / .tscale (+ .zero when the trace crosses zero), all tagged {id} + {id}.scopes; the trace is a plain polyline, so draw traces it on |
Grid
| API | Kind | What it does |
|---|---|---|
grid(id: new id, [center: (x, y)], [cols: number], [rows: number], [cellsize: number], [seeded_cellsize: number]) | constructor / setup | a 2D cell grid (arg1 a “# . @ *” spec seeds it, else it is the centre) |
neighbors(id: id or tag, mode: string) | constructor / setup | grid connectivity: “4” (default) or “8” (diagonals) |
setcell(id: id or tag, r: number, c: number, kind: id or tag) | constructor / setup | set one cell’s kind (wall/open/start/goal) |
walls(id: id or tag, cells: string) | constructor / setup | batch-set several cells to wall: “r,c r,c …” |
gridbfs(id: id or tag, start: (x, y), goal: (x, y)) | state-changing verb | unweighted BFS over open cells (spatial sibling of bfs) |
gridastar(id: id or tag, start: (x, y), goal: (x, y), [heuristic: id or tag]) | state-changing verb | A* pathfinding with a heuristic (manhattan/euclidean/diagonal) |
evolve(id: id or tag, rule: string) | constructor / setup | pre-simulate one CA generation (“life” or a Golly rulestring) |
collapse(id: id or tag, tileset: string, [seed: number]) | constructor / setup | pre-simulate a seeded Wave-Function-Collapse settling |
Chem
| API | Kind | What it does |
|---|---|---|
structure(id: new id, source: string, [center: (x, y)], [unit: number], [spec: string]) | constructor / setup | a SKELETAL STRUCTURAL FORMULA — the flat drawing a chemist puts on paper — from an MDL structure file’s 2-D depiction coordinates. Every convention is derived from the file, not authored: a carbon is a bare VERTEX, hydrogens on carbon vanish, a heteroatom is lettered with its hydrogens folded in (an oxygen with one explicit H becomes OH, a nitrogen with two becomes NH2), a double bond’s second line leans toward the CROWDED side — which inside a ring is the ring centre, and falls out of the neighbours’ own coordinates without detecting rings — and a bond is a wedge or a hash where the molfile’s fourth bond column records stereochemistry. Bonds stop short of a letter so no line runs under text. unit is PIXELS PER BOND (default 64; a 2-D record is laid out on a 1.0-per-bond grid, so this is literally how big a bond should be). spec takes carbons=1 (letter every carbon too — the explicit style a first lesson uses), hydrogens=1 (draw them as their own vertices instead of folding them in), labels=text|latex (latex TYPESETS the label so a subscript is a real subscript — NH₂ rather than NH2 — and a charge is raised; costs one rasterised image per DISTINCT label, so a molecule with six hydroxyls costs one), color=, record=. Entities: bonds {id}.b{k} (the FILE’s numbering, so it has gaps where C–H bonds were skipped — address {id}.bonds instead), labels {id}.a{i}, tagged {id} + {id}.bonds / {id}.labels / {id}.multibonds / {id}.wedges / {id}.hashes, and every label also by its element ({id}.O, {id}.N) — and by FUNCTIONAL GROUP: {id}.carboxyl, {id}.ester, {id}.hydroxyl, {id}.aromatic and the rest of hydroxyl / carbonyl / carboxyl / aldehyde / ketone / ester / amide / primary_amine / secondary_amine / tertiary_amine / nitro / nitrile / thiol / sulfide / sulfoxide / sulfone / halogen / ether / epoxide / aromatic, detected from the structure itself, so color(asp.carboxyl, crimson) addresses the CHEMISTRY rather than a list of indices. A tag covers the group’s labels AND the bonds inside it, every repeat of a group shares one tag (a sugar’s five hydroxyls colour together), and groups OVERLAP on purpose — a carboxyl is also a carbonyl and a hydroxyl, all three being true of it. Works identically from a file or a SMILES string. NAMES: {id}.name (the everyday word, from the curated asset:molecules/ filename) and {id}.iupac (the systematic name, from the record or generated) are HIDDEN captions under the drawing — show(caf.name); show(caf.iupac); is the nomenclature lesson. A SMILES string or your own file gets {id}.iupac at best and no {id}.name; long names wrap. The captions are tagged {id}.names and NOT {id}, so show({id}) does not reveal them. EVERY atom is addressable as {id}.a{i} even when it carries no letter — a skeletal carbon gets an invisible anchor — so arrow(a, nuc.a0, sub.a1, 62) aims a curly arrow at a carbocation, and {id}.b{k} resolves to a bond’s midpoint. A structure that COULD NOT EXIST is refused rather than drawn — five bonds on a carbon lays out cleanly, so only a valence check catches it; the rule is charge-aware, so [N+](C)(C)(C)C is fine and O(C)(C)C is not. names wrap. These are 2-D entities, so framebox and brace work on them (unlike molecule3), and untraced + stagger { draw({id}.bonds) } writes the formula on bond by bond. Needs the 2-D record — a 3-D conformer flattened is a tangle of crossing bonds, so one is refused with the record_type=2d fix named. Bundled as asset:molecules/<name>-2d.sdf. source ALSO takes a SMILES string in place of a path — structure(e, "CCO") is ethanol — and then the coordinates are COMPUTED, not read: a chain zig-zags at the tetrahedral angle, a ring closes as a regular polygon, a fused ring reflects across the bond it shares, a substituent points radially out of its ring, and an aromatic ring is Kekulé-ised so the alternating double bonds are real bonds. A string is read as SMILES when it does not look like a file (no asset:, no /, no .sdf/.mol), so the two forms never collide. The layout is CHECKED before it is drawn — overlapping atoms, crossing bonds or a stretched bond are REFUSED with the 2-D-file fix named, rather than drawn as a tangle, because a plausible-but-wrong formula is worse than none. Chains, single rings and outward-growing FUSED systems are reliable, and that covers a steroid’s four rings, a substituted phenanthroline and the aromatics; a BRIDGED or caged skeleton (morphine) is what gets refused. SMILES atoms are numbered in WRITTEN order, so CC(=O)O puts its hydroxyl oxygen last and {id}.a4 addresses it |
molecule3(id: new id, path: string, [center: (x, y, z)], [scale: number], [style: id or tag], [record: number]) | constructor / setup | a REAL MOLECULE from an MDL structure file — .sdf (what a PubChem “Download → SDF” gives you) or .mol, as an asset: URI or a path. Nothing about the shape is authored: the atom coordinates are the file’s, in Ångströms, so a bond angle on screen is the one someone computed. Atoms are spheres coloured by the CPK/Jmol convention and sized by covalent radius; bonds are sticks, with a double bond drawn as two parallel lines and a triple as three. center defaults to the origin and the molecule is centred on its own centroid; scale is world units per Ångström (default 1, which puts a small molecule at the few-units scale the rest of the 3D kit works at); style is ball (default), spacefill (full radii, no bonds — the shape the molecule presents to the world) or wireframe (bonds only); record picks one molecule out of a multi-record SDF, counting from 1. Atoms are {id}.a{i} and bonds {id}.b{k} ({id}.b{k}.{n} for each line of a multiple bond), tagged {id} + {id}.atoms / {id}.bonds / {id}.multibonds — and every atom is ALSO tagged by its element ({id}.O, {id}.C), so color(caf.N, cyan) addresses the chemistry rather than an index. Bundled: water, methane, ammonia, benzene, carbon-dioxide under asset:molecules/. A V3000 file is refused with the way to convert it |
balance(id: new id, center: (x, y), equation: string, [size: number]) | constructor / setup | solve a chemical equation’s coefficients and draw the SKELETON: balance(rx, (x,y), “Fe + O2 -> Fe2O3”). One column per species and one row per element makes a matrix whose null space IS the set of balanced equations, so the coefficients are found by exact rational elimination (never floating point) and reduced to the smallest whole numbers - 4 Fe + 3 O2 -> 2 Fe2O3. Formulae read as written, with nested groups Ca(OH)2 / Al2(SO4)3 and hydrates CuSO4.5H2O, and every symbol is checked against the periodic table (Co is cobalt, CO is carbon monoxide). An equation that cannot balance says which: an element that only appears on one side, two reactions written as one, or something on the wrong side of the arrow. The equation is an ordinary LaTeX image at {id}; solve(id) writes the coefficients in |
tally(id: id or tag, center: (x, y), [width: number], [row: number], [size: number]) | constructor / setup | the conservation table beside a balance: one row per element, atoms on the left against atoms on the right, red while they disagree and lime when they match. Counts are live counters that solve updates at every step, so a half-finished balance shows exactly the mismatch a student would be looking at. Draws {id}.tally.e{k} + {id}.tally.l{k}/{id}.tally.r{k}, all tagged {id}.tally |
solve(id: id or tag, [dur: number]) | state-changing verb | write a balance’s coefficients in, one at a time, with the tally keeping score. Each step is a rewrite, so a coefficient slides in beside a formula that stays exactly where it is - the equation is never retyped or re-laid-out. dur is the total for all the steps (default 2.4). An equation that needs no coefficients still plays; it simply has nothing to add |
lewis(id: new id, formula: string, center: (x, y), [unit: number], [size: number]) | constructor / setup | a Lewis structure WORKED from a formula: lewis(x, “NO3-”) counts the valence electrons, picks the central atom (least electronegative that can hold more than one bond), spends two on each bond, completes the octets from the outside in, promotes a lone pair to a pi bond while the centre is short (or while it tidies the formal charges, where period 3+ may expand), and computes the formal charges - which must sum to the ion’s charge or it refuses. Charges beyond +-1 want a separator: “SO4 2-” or “CO3^2-”, since NO3- is nitrate rather than NO with three minuses. Electron-deficient centres stay deficient (BF3/BeCl2/AlCl3 keep single bonds and no charges, because a halogen does not pi-donate) while carbon still insists on its octet (CO is a triple bond with C- and O+). Oxyacids put their hydrogens on oxygen and complete those oxygens before the centre, so H2SO4 comes out neutral with two S=O. Declines what it cannot honestly draw: an ODD electron count (NO, NO2 - radicals), transition metals (no group number to count), and any skeleton needing TWO central atoms (C2H6, H2O2, CH3OH - a formula cannot say H-O-O-H rather than H2O-O). Draws {id}.a{i} letters + {id}.bonds lines (one per pair, so a double is two lines) + {id}.pairs dots + {id}.charges + an {id}.count electron total + {id}.formula, which labels the structure with what it is OF (NO3- prints as NO3- with the digit down and the charge up). The NAME is not the kit’s to give: nitrate is a fact about usage, so a scene says that |
octet(id: id or tag, [dur: number]) | animation verb | play the working of a lewis structure in the order it is taught: the electron count, then the sigma bonds one at a time, then the lone pairs from the outside in, then the pair that SWINGS IN to finish the centre’s octet, then the formal charges that fall out of it. The step that needs the animation is the fourth one |
resonate(id: id or tag, [dur: number], [cycles: number]) | animation verb | move a lewis structure’s extra bonds round its equivalent neighbours - the double bond hopping between the three oxygens of nitrate, or the two of ozone - with the lone pairs and formal charges following it, because they are the same bookkeeping. The claim being made is that the molecule is NONE of the drawings and the average of all of them, which is only sayable in motion. Refuses when there is only one arrangement |
levels(id: new id, center: (x, y), [w: number], [h: number], [nmax: number], [Z: number]) | constructor / setup | the energy-level diagram of a ONE-ELECTRON atom (H, He+, Li2+ …): E_n = -13.606 Z^2/n^2 eV, so the rungs are placed at their energies and crowd towards zero as n grows. Every transition’s photon is computed with lambda = hc/dE. Restricted to one electron on purpose - the Rydberg formula is exact there and simply wrong for anything with electron-electron repulsion. Draws {id}.n{k} rungs (tagged {id}.rungs) + labels + {id}.electron + {id}.photon + {id}.readout |
emission(id: new id, levels: id or tag, center: (x, y), [w: number], [h: number], [from: number]) | constructor / setup | the emission spectrum those levels produce: one line per VISIBLE transition, standing at its own wavelength, and COLOURED FROM that wavelength rather than chosen - so hydrogen’s Balmer alpha is red at 656 nm because the arithmetic put it there. Ultraviolet and infrared transitions are computed but not drawn, because they are not colours. Draws {id}.strip + {id}.line{k} (tagged {id}.lines) + wavelength labels |
drop(id: id or tag, from: number, to: number, [dur: number]) | animation verb | an electron falls from one level to another and emits the photon that energy difference is worth: the electron slides down the diagram, a photon of the computed COLOUR flies off, the matching spectral line flares, and the readout gives n, the energy in eV and the wavelength in nm. Emission only - the second level must be the lower one |
cell(id: new id, metals: string, center: (x, y), [w: number], [h: number], [spec: string]) | constructor / setup | a galvanic cell that works out its own polarity: give it two metals (“Zn|Cu”) and the more positive standard reduction potential becomes the CATHODE, so “Zn|Cu” and “Cu|Zn” are the same cell and both read +1.10 V. E(cell) = E(cathode) - E(anode), I = E/R across the external resistor, and the half-equations are written from the same decision. spec is a “key=value” string: resistance (ohms, default 10), carriers (electrons drawn, default 8). Draws beakers, electrodes, salt bridge, wire, a voltmeter reading the computed EMF, plus {id}.electrons and {id}.ions; the two counter captions carry {id}.captions rather than {id}, so show(id) is the bench alone |
discharge(id: id or tag, [dur: number], [minutes: number]) | animation verb | let a cell run: electrons cross the wire and ions cross the salt bridge at a rate set by the current, while the readouts count the charge (Q = It) and the anode’s mass loss (Faraday: m = MQ/nF). minutes is how long the cell is being run for, not how long the shot is - a Daniell cell through 10 ohms for half an hour is 198 C and 67 mg of zinc, and that is what it says rather than an exaggerated electrode |
lattice(id: new id, formula: string, center: (x, y), [cols: number], [rows: number], [unit: number]) | constructor / setup | an ionic lattice as an alternating grid, sized by real ionic radii (chloride really is much bigger than sodium). 1:1 salts only, since that is what a checkerboard keeps honest. Counts each ion’s COORDINATION - 2 at a corner, 3 on an edge, 4 inside - which is the order dissolve uses. Where a data book has the numbers it also prints the energy account: lattice enthalpy against the hydration enthalpies, and the small positive dH that explains why dissolving salt is entropy-driven. Draws {id}.i{k} ions (tagged {id}.ions) + labels + waiting {id}.water; the spoken-word readout carries {id}.captions rather than {id}, so show(id) is the crystal alone |
dissolve(id: id or tag, [dur: number], [count: number]) | animation verb | the crystal comes apart, CORNERS FIRST - ions leave in order of how few neighbours hold them, which is why a crystal erodes inwards. Each one leaves inside a hydration shell whose water molecules are turned by the sign of its charge: oxygen towards a cation, hydrogens towards an anion. count is how many ions leave |
newman(id: new id, source: string, center: (x, y), [unit: number], [labels: number]) | constructor / setup | a Newman projection down a molecule’s central rotatable bond (chosen for you: the single bond with the most on both sides), with its torsional energy profile COMPUTED - the real geometry is rotated degree by degree and scored with a threefold torsional term plus Lennard-Jones repulsion across the bond. Butane comes out with staggered wells, anti lowest and the methyls eclipsing worst. It is a RIGID scan, so peaks come out high (butane’s syn barrier lands in the fifties against a measured 19-21) - the order and the shape are the lesson. Draws {id}.circle + {id}.front + {id}.back arms + {id}.readout |
profile(id: new id, torsion: id or tag, center: (x, y), [w: number], [h: number]) | constructor / setup | the energy-against-dihedral curve for a newman, in kJ/mol above the lowest conformer, with a marker that rides it while twist turns the bond. Ticks every 60 degrees, which is where staggered and eclipsed alternate |
twist(id: id or tag, angle: number, [dur: number]) | state-changing verb | turn a newman to a dihedral angle, the short way round, with the profile marker climbing or falling to the energy that arrangement costs. The readout names it the way a chemist does - anti, gauche, eclipsed - and gives the energy. State carries forward, so a chain of twists walks the curve |
vibration(id: new id, source: string, center: (x, y), [unit: number], [labels: number]) | constructor / setup | a molecule drawn flat with its NORMAL MODES computed from real geometry: masses from the periodic table, a harmonic force field of bond, angle and out-of-plane springs with tabulated force constants, and the eigenvectors of the mass-weighted Hessian. Gives the textbook set - 3N-6 modes, or 3N-5 when the molecule is linear - with wavenumbers in cm^-1 that land near the measured ones (H-Cl 2886, CO2 asymmetric stretch 2374 against 2349). Needs the 3-D record, not a flat depiction, because the shape is the physics. Draws {id}.b{k} bonds + {id}.a{i} atoms (tagged {id}.atoms) + {id}.l{i} labels + a {id}.readout that vibrate fills in |
vibrate(id: id or tag, mode: number, [dur: number], [amp: number], [cycles: number]) | animation verb | animate one normal mode of a vibration, numbered from the lowest wavenumber up. The atoms move along the mode’s own eigenvector, so a symmetric stretch really is symmetric; amp scales the displacement (0.28 of a bond by default) and cycles sets how many oscillations fit in dur. Sets {id}.readout to the wavenumber, whether it is a stretch or a bend (decided by what the motion does to bond lengths against bond angles), and whether it is infrared active |
irspectrum(id: new id, molecule: id or tag, center: (x, y), [w: number], [h: number], [size: number]) | constructor / setup | the infrared spectrum a molecule’s modes imply, drawn high wavenumber to low as a spectrum is: a Lorentzian band per ACTIVE mode with its wavenumber labelled, and a coral marker where a SILENT one would have been. A mode is active only if it changes the dipole moment - carbon dioxide’s symmetric stretch does not, so it has no peak however hard the bond is vibrating, and that cancellation is computed from the mode’s own displacements rather than asserted. Draws {id}.axis + {id}.curve + {id}.peaks + {id}.silent |
supply(id: id or tag, amounts: string) | constructor / setup | what you are GIVEN of each reactant, for a balance: supply(rx, “Fe=10g O2=5g”). Values are grams unless written mol (Fe=0.18mol), converted with STANDARD atomic weights - the ones you weigh with, not the monoisotopic masses a mass spectrum uses. Every reactant needs an amount, because the question is which of them runs out first. Solving it is one line: batches = moles / coefficient, and the smallest wins - which is why the limiting reagent is so often NOT the one you have least of |
limiting(id: id or tag, center: (x, y), [width: number], [row: number], [size: number]) | constructor / setup | the limiting-reagent view for a balance that has a supply: one bar per reactant showing how far the reaction could get on that reactant alone (moles / coefficient), the shortest marked runs out first, then what comes out - each product’s mass, the excess left standing, and a mass-in against mass-out check that has to agree. Everything starts blank and arrives during react. Draws {id}.limit.b{k} bars + {id}.limit.n{k} / .y{p} / .spare / .in / .out counters, tagged {id}.limit |
react(id: id or tag, [dur: number]) | animation verb | run the supplied amounts through a balanced equation: the bars grow to how far each reactant alone could take it, the shortest is named as the limiting reagent, and the yields, the leftover and the mass check count up. Needs supply and limiting first |
Brand
| API | Kind | What it does |
|---|---|---|
banner(id: new id, center: (x, y), [scale: number]) | constructor / setup | the manic logo/banner |
watermark(id: new id, [at: (x, y)], [text: string]) | constructor / setup | a screen-fixed watermark; omit the point for responsive bottom-right placement or pass an exact point |
Stats
| API | Kind | What it does |
|---|---|---|
histogram(id: new id, center: (x, y), data: string, [bins: number], [width: number], [height: number], [color: colour]) | constructor / setup | bin a dataset into bars — the shape of the data |
summary(id: new id, center: (x, y), data: string, [width: number], [color: colour]) | constructor / setup | describe a dataset: mean/median/mode markers, spread, and readouts |
correlation(id: new id, center: (x, y), unit: number, data: string, [color: colour]) | constructor / setup | scatter + best-fit line + the Pearson correlation r |
skew(id: new id, center: (x, y), data: string, [bins: number], [width: number], [height: number], [color: colour]) | constructor / setup | a histogram labelled with its skewness (left / right / symmetric) |
boxplot(id: new id, center: (x, y), data: string, [width: number], [color: colour]) | constructor / setup | five-number summary as a box-and-whisker (box = IQR) |
hypothesis(id: new id, center: (x, y), z: number, [alpha: number], [unit: number]) | constructor / setup | a two-tailed significance test: p-value as shaded normal tails |
covariance(id: new id, center: (x, y), unit: number, data: string, [color: colour]) | constructor / setup | covariance as signed-area rectangles about the mean cross |
bayes(id: new id, center: (x, y), heads: number, tails: number, [width: number], [height: number]) | constructor / setup | Bayesian updating: prior + likelihood -> posterior for a coin’s bias |
distribution(id: new id, center: (x, y), kind: string, a: number, [b: number], [color: colour]) | constructor / setup | a named distribution: uniform / exponential / maxwell / binomial / poisson |
confidence(id: new id, center: (x, y), mean: number, sd: number, n: number, [level: number], [width: number]) | constructor / setup | a confidence interval for a mean: estimate + z*sd/sqrt(n) error bar |
montecarlo(id: new id, center: (x, y), points: number, [seed: number], [size: number]) | constructor / setup | estimate pi by darts: random points in a square vs the circle |
randomwalk(id: new id, center: (x, y), steps: number, [seed: number], [scale: number]) | constructor / setup | a 2D random walk path from the centre |
bellcurve(id: new id, center: (x, y), mu: number, sigma: number, [unit: number], [color: colour]) | constructor / setup | the normal/Gaussian bell curve with the 68-95-99.7 rule shaded |
gaussian(id: new id, center: (x, y), mu: number, sigma: number, [unit: number], [color: colour]) | constructor / setup | the normal/Gaussian bell curve with the 68-95-99.7 rule shaded |
lln(id: new id, center: (x, y), trials: number, [seed: number], [width: number], [height: number]) | constructor / setup | Law of Large Numbers: a running proportion settling onto the truth |
clt(id: new id, center: (x, y), samplesize: number, trials: number, [seed: number], [width: number], [height: number], [color: colour]) | constructor / setup | Central Limit Theorem: sample means of dice pile into a bell |
Physics
| API | Kind | What it does |
|---|---|---|
freekick(id: new id, center: (x, y), speed: number, angle: number, spin: number, [scale: number], [steps: number]) | constructor / setup | an RK4 free-kick trajectory under drag + the Magnus force (sidespin curves it); draw {id}, run(id) flies the ball |
pendulum(id: new id, [center: (x, y)], [length: number], [angle0: number], [unit: number], [damping: number]) | constructor / setup | a swinging pendulum (pre-simulated); animate with swing(id) |
spring(id: new id, [center: (x, y)], [stiffness: number], [x0: number], [unit: number], [damping: number], [v0: number]) | constructor / setup | a mass on a spring (pre-simulated); animate with run(id) |
doublependulum(id: new id, [center: (x, y)], [angle1: number], [angle2: number], [unit: number]) | constructor / setup | the chaotic double pendulum (pre-simulated); animate with run(id) |
springpendulum(id: new id, [center: (x, y)], [angle0: number], [stretch0: number], [unit: number], [damping: number]) | constructor / setup | an elastic pendulum (swings + bounces); animate with run(id) |
kapitza(id: new id, [center: (x, y)], [angle0: number], [vibeamp: number], [unit: number]) | constructor / setup | a Kapitza pendulum — fast vibration stabilises the inverted position |
cartpendulum(id: new id, [center: (x, y)], [angle0: number], [unit: number]) | constructor / setup | a pendulum on a spring-mounted cart (control-theory system) |
comparependulum(id: new id, [center: (x, y)], [angle0: number], [unit: number]) | constructor / setup | two chaotic pendulums a hair apart — sensitive dependence |
verticalspring(id: new id, [center: (x, y)], [stretch0: number], [unit: number], [damping: number]) | constructor / setup | a mass bobbing on a vertical spring under gravity |
springincline(id: new id, [center: (x, y)], [angle: number], [unit: number], [damping: number]) | constructor / setup | a mass on a spring on an inclined plane |
bungee(id: new id, [center: (x, y)], [unit: number], [damping: number]) | constructor / setup | a bungee jump — free-fall then a one-sided elastic cord |
resonance(id: new id, [center: (x, y)], [drivefreq: number], [unit: number], [damping: number]) | constructor / setup | a driven spring — amplitude peaks near the natural frequency |
doublespring(id: new id, [center: (x, y)], [unit: number]) | constructor / setup | two masses coupled by springs — beating / normal modes |
seriesparallel(id: new id, [center: (x, y)], [unit: number]) | constructor / setup | springs in series vs parallel — soft/slow vs stiff/fast |
carsuspension(id: new id, [center: (x, y)], [unit: number]) | constructor / setup | a quarter-car riding a scrolling road (bump/washboard/pothole) |
piston(id: new id, [center: (x, y)], [rpm: number], [unit: number]) | constructor / setup | an engine piston — a spinning crank drives a slider in a cylinder |
molecule(id: new id, [center: (x, y)], [atoms: number], [unit: number]) | constructor / setup | N atoms bonded by springs, vibrating about their shape |
robotarm(id: new id, [center: (x, y)], [mode: number], [unit: number]) | constructor / setup | a two-link robot arm tracking a target by inverse kinematics (mode 1=circle, 2=figure-8, 0=fixed) |
pulley(id: new id, [center: (x, y)], [m1: number], [m2: number], [unit: number]) | constructor / setup | a vertical Atwood machine — two masses over one pulley |
pulleyscale(id: new id, [center: (x, y)], [m1: number], [m2: number], [unit: number]) | constructor / setup | an Atwood machine with an in-line spring scale reading the rope tension |
blocktackle(id: new id, [center: (x, y)], [load: number], [effort: number], [strands: number], [unit: number]) | constructor / setup | a compound pulley (block & tackle): N strands give a mechanical advantage of N |
compoundpulley(id: new id, [center: (x, y)], [mA: number], [mB: number], [mC: number], [unit: number]) | constructor / setup | a compound pulley: a fixed pulley + a movable pulley with three masses A, B, C (static when mA = mB+mC) |
ramp(id: new id, [center: (x, y)], [angle: number], [mass: number], [applied: number], [unit: number]) | constructor / setup | a block sliding on an inclined plane with friction |
dropmass(id: new id, [center: (x, y)], [dropheight: number], [unit: number]) | constructor / setup | a mass dropped onto a spring-block — inelastic collision, energy lost |
inclinepulley(id: new id, [center: (x, y)], [angle: number], [m1: number], [m2: number], [unit: number]) | constructor / setup | a block on an incline tied over a top pulley to a hanging mass (incline-Atwood) |
doubleincline(id: new id, [center: (x, y)], [angle1: number], [angle2: number], [m1: number], [m2: number], [unit: number]) | constructor / setup | two blocks on a wedge’s two slopes, tied over an apex pulley (right slope rough) |
inclinebumper(id: new id, [center: (x, y)], [angle: number], [mass: number], [stiffness: number], [unit: number]) | constructor / setup | a block slides down an incline into a spring bumper at the base, then bounces back |
springchain(id: new id, [center: (x, y)], [angle: number], [unit: number]) | constructor / setup | three blocks joined by two springs on an incline — coupled oscillators / normal modes |
looptrack(id: new id, [center: (x, y)], [radius: number], [height: number], [unit: number]) | constructor / setup | a ball rolls down a ramp and around a vertical loop-the-loop (curved track) |
collideblocks(id: new id, [center: (x, y)], [m1: number], [m2: number], [restitution: number], [unit: number]) | constructor / setup | two blocks collide on a frictionless track with elastic walls (restitution e) |
bulletblock(id: new id, [center: (x, y)], [bulletmass: number], [speed: number], [blockmass: number], [unit: number]) | constructor / setup | a bullet fired into a block embeds (perfectly inelastic — most kinetic energy lost) |
newtonscradle(id: new id, [center: (x, y)], [balls: number], [pulled: number]) | constructor / setup | Newton’s cradle — pull N balls, N swing out (event-driven elastic collisions) |
gas(id: new id, [center: (x, y)], [w: number], [h: number], [count: number], [spec: string]) | constructor / setup | N hard discs bouncing elastically in a box — a general particle gas, not a chemistry builtin. Speeds are drawn from the 2-D Maxwell-Boltzmann distribution (or start=equal, to watch collisions PRODUCE that distribution), and energy is conserved by the bounces. spec is a “key=value” string: temperature (kT, the energy unit), radius (px), mass, scale (px per length unit - the density/liveliness knob), dt, steps (frames of trajectory), seed, start=maxwell|equal. Lays out {id}.box + {id}.p{i}, tagged {id} and {id}.particles; animate with run(id, secs). Add species to label populations and rule to make collisions change them; timegraph(id, ...) then draws the populations over time |
species(id: id or tag, name: new id, [weight: number], [colour: colour]) | constructor / setup | label a sub-population of a gas: species(g, A, 0.5, cyan) starts half the particles as A. Weights are relative, and a weight of 0 (or the short form species(g, C, gold)) is a population that starts EMPTY - a product waiting to be made. Colouring the species colours its particles, so the mix is readable at t=0 |
rule(id: id or tag, rule: string) | constructor / setup | a collision-triggered transition in a gas, written the way it goes on a board: rule(g, “A + B -> C + C when energy > 3”). The threshold is the relative kinetic energy along the line of centres in the SAME unit as temperature, so Ea/kT is the Boltzmann exponent - raise the temperature and more collisions clear the barrier, on their own. Drop the when clause and every meeting counts, which is how the same mechanism becomes an infection (“S + I -> I”) or a chain reaction. One product name is short for both participants becoming it; products need no prior species declaration |
speeds(id: id or tag, center: (x, y), [w: number], [h: number], [bins: number], [smooth: number], [vmax: number]) | constructor / setup | a LIVE histogram of a gas’s own speeds, with the exact Maxwell-Boltzmann prediction drawn over it on the same scale. The bars are ordinary rectangles that run(id) grows and shrinks from the same trajectory that moves the particles, so the distribution is MEASURED, not illustrated - start the gas with start=equal and watch collisions build the bell out of a single spike. smooth is how many frames each bar averages (default 9, a stated running average over a noisy small sample), and vmax sets the top of the speed axis explicitly (default 3.3 sigma) so two gases at different temperatures can share one axis. Draws {id}.speeds.axis + {id}.speeds.b{k} (tagged {id}.speeds.bars) + {id}.speeds.mb, all tagged {id}.speeds |
dominos(id: new id, [center: (x, y)], [count: number], [height: number], [thickness: number], [spacing: number], [unit: number], [transfer: number], [push: number]) | constructor / setup | a row of dominos toppling as a wave — rigid slabs pivoting on their base edge; lengths in MILLIMETRES (height 45, thickness 7.5438, spacing 9.38 are real domino numbers), unit = px per mm, transfer = 0-1 fraction of angular speed handed on at each impact, push = the first domino’s nudge (rad/s). The wave speed is not a parameter: it emerges from spacing/height/thickness/transfer, so wide spacing kills the chain. Lays out {id}.ground + {id}.d{i}, tagged {id}; animate with run(id) |
dominopath(id: new id, center: (x, y), sx: number, sy: number, x(t): string, y(t): string, [count: number], [tdomain: (x, y)], [height: number], [thickness: number], [unit: number], [transfer: number], [push: number]) | constructor / setup | dominos standing along ANY parametric curve, toppling as a wave that follows the shape - seen from ABOVE, so a standing domino is a short bar and a fallen one is a long bar lying forward (footprint = thicknesscos + heightsin). Curve written like param: x(t)/y(t) through (cx,cy) + sx/sy over tdomain (default (0,tau)) - a spiral, heart or lissajous becomes a domino run. count SETS the spacing (equal arc length), so more dominos = tighter gaps = slower wave; overlapping or unreachable spacing is a clear error. Same slab physics as dominos. Lays out {id}.path + {id}.d{i}, tagged {id}; stagger a show over {id}.d{i} to place them one by one, then run(id) |
stringwave(id: new id, [center: (x, y)], [width: number], [amp: number], [pluck: number]) | constructor / setup | a wave on a plucked string (N masses on springs, fixed ends) — the wave equation |
raft(id: new id, [center: (x, y)], [personmass: number], [raftmass: number], [unit: number]) | constructor / setup | a person walking on a floating raft — centre of mass stays fixed |
brachistochrone(id: new id, [center: (x, y)], [unit: number]) | constructor / setup | four beads race down different curves under gravity — the cycloid wins |
run(id: id or tag, [dur_or_gens: number], [dur: number]) | animation verb | replay a sim’s motion over [dur] s (or a grid’s frames: run(grid, [gens], [dur])) |
swing(id: id or tag, [dur: number]) | animation verb | replay a pendulum’s motion over [dur] seconds (alias of run) |
forces(id: id or tag, [dur: number]) | animation verb | reveal a sim’s free-body force diagram (ramp: gravity, normal, friction, a) |
phase(id: id or tag, center: (x, y), [size: number]) | constructor / setup | a sim’s phase portrait (e.g. θ vs ω); animates with swing |
well(id: id or tag, center: (x, y), [size: number]) | constructor / setup | a sim’s potential-energy well with the body as a rolling ball |
timegraph(id: id or tag, center: (x, y), [size: number]) | constructor / setup | a sim’s phase variables as curves over time (with a sweep line) |
energygraph(id: id or tag, center: (x, y), [size: number]) | constructor / setup | KE/PE/total energy as curves over time (with a sweep line) |
Rubik
| API | Kind | What it does |
|---|---|---|
cube(id: new id, n: number, center: (x, y), [size: number], [view: new id], [state: string], [scramble: number]) | constructor / setup | a Rubik’s cube of ANY size (n = 2..9) with one view of it. view = net (the unfolded 4Nx3N cross, default) or trefoil (every sticker a dot on three families of rings, so a turn slides them along their ring and the permutation is visible travelling between faces). size = sticker pitch (net) or overall span (trefoil). state = “solved” (default), a facelet string in face order U R F D L B (a scanned or solver state pastes straight in), or a scramble in move notation like “R U R’ F2”. Stickers are ordinary entities {id}.s{i} tagged {id} + {id}.stickers + their face ({id}.U … {id}.B), so show/fade/color/pulse/framebox all work; being self-contained it also sweeps over n |
rubik3(id: new id, n: number, [size: number], [state: string], [scramble: number]) | constructor / setup | the same cube as an OBJECT in 3-D: one thin plate per sticker just proud of the surface, so the state model and moves are the ones the flat views use. A turn rotates that layer’s plates about the cube’s axis. size = the cube’s edge in world units (default 2); frame it with camera3 and spin it with orbit3. Plates are ordinary 3-D entities {id}.s{i} tagged {id} + {id}.stickers + their face. state takes the same spellings as cube |
moves(id: id or tag, sequence: string, [dur: number], [ease: easing]) | state-changing verb | apply a move sequence to a cube, animated: “R U R’ U’”. Notation is the cuber’s own, so scrambles and solutions paste straight in - outer turns R R’ R2, wide Rw/3Rw/lowercase r, inner slices 2R..ceil(n/2)R, whole-cube rotations x y z, and the 3x3 middles M E S. Every intermediate state is computed at build time, so it scrubs exactly and consecutive calls continue from where the last left off. This kit animates moves; it does not search for them - feed a solver’s solution to return to solved |
Machine learning
| API | Kind | What it does |
|---|---|---|
network(id: new id, center: (x, y), layers: string, activations: string, [width: number], [height: number], [seed: number]) | constructor / setup | a deterministic feed-forward neural network with responsive layered layout and stable node/edge tags |
activation(id: new id, center: (x, y), function: id or tag, [width: number], [height: number]) | constructor / setup | a truthful plot of linear, ReLU, sigmoid, or tanh |
forward(network: id or tag, input: string, [dur: number], [ease: easing]) | state-changing verb | compute and progressively reveal one feed-forward pass with real activations and output values |
feed(network: id or tag, image: id or tag, [dur: number], [ease: easing]) | state-changing verb | the image->input bridge: flatten a tensor/image into the network input, draw connector lines into the input nodes, and run the forward pass (tensor cell count must equal the input-layer width) |
loss(network: id or tag, target: string, [kind: id or tag], [dur: number], [ease: easing]) | state-changing verb | compare the latest network output with a target using cross-entropy or mean-squared error |
backward(network: id or tag, [dur: number], [ease: easing]) | state-changing verb | compute exact reverse-mode gradients and reveal them from output to input |
update(network: id or tag, [learning_rate: number], [dur: number], [ease: easing]) | state-changing verb | apply one gradient-descent parameter update and recompute the prediction and loss |
checkpoint(id: new id, network: id or tag) | state-changing verb | save one network’s exact parameters, prediction, target, and loss for a later rollback |
restore(target: id or tag, [checkpoint: id or tag], [dur: number], [ease: easing]) | state-changing verb | restore(target, …): a network form restore(network, checkpoint, [dur], [ease]) exactly rolls back a checkpointed network state; the general form restore(id, [dur], [ease]) animates ANY entity that was savestated back to its snapshot (pos/scale/rotation/colour/opacity) |
tensor(id: new id, center: (x, y), values: string, [cell: number], [color: colour]) | constructor / setup | a finite numeric grid; semicolons separate rows and vertical bars separate channels. Draw it instead as pixel-art with #=ink, .=blank, 1-9 = grayscale (a # anywhere switches to art mode; whitespace is ignored) |
digit(id: new id, center: (x, y), digit: string, [cell: number], [color: colour]) | constructor / setup | a stylised handwritten digit 0-9 as a 12x12 pixel-art tensor (144 cells), ready to feed into a 144-input network |
kernel(id: new id, center: (x, y), values: string, [cell: number], [color: colour]) | constructor / setup | a finite convolution kernel with one grid per input channel |
convolve(id: new id, input: id or tag, kernel: id or tag, center: (x, y), [stride: number], [padding: number], [bias: number], [activation: id or tag], [cell: number]) | constructor / setup | compute a feature map from an input tensor and kernel with validated stride, padding, bias, and activation |
pool(id: new id, input: id or tag, center: (x, y), kind: id or tag, [window: number], [stride: number], [padding: number], [cell: number]) | constructor / setup | compute deterministic max or average pooling independently across tensor channels |
scan(output: id or tag, [dur: number], [ease: easing]) | state-changing verb | coordinate the receptive field, operator, arithmetic, and destination cell of a convolution or pool |
tokenize(id: new id, center: (x, y), text: string, [mode: id or tag], [width: number]) | constructor / setup | split text into a small deterministic token sequence using authored, word, or character boundaries |
embedding(id: new id, tokens: id or tag, center: (x, y), vectors: string, [position: id or tag], [width: number], [height: number]) | constructor / setup | turn a token sequence into explicit or seeded educational vectors and add sinusoidal or no positional encoding |
transformer(id: new id, embedding: id or tag, center: (x, y), config: string, [width: number], [height: number]) | constructor / setup | compute and draw a complete deterministic transformer block from an ML5 embedding and a compact heads/mask/MLP/norm/dropout specification |
encode(transformer: id or tag, [dur: number], [ease: easing]) | state-changing verb | reveal a transformer block through heads, masking, concatenation, residuals, normalization, MLP, dropout, and output |
logits(id: new id, transformer: id or tag, token: number, center: (x, y), labels: string, [temperature: number], [width: number], [height: number], [seed: number]) | constructor / setup | project one transformer hidden row into an authored educational vocabulary and compute the full temperature-scaled softmax |
sample(logits: id or tag, strategy: string, [dur: number], [ease: easing]) | state-changing verb | filter, renormalize, and deterministically select from a logits view using greedy, categorical, top-k, or top-p decoding |
attention(id: new id, center: (x, y), tokens: string, embeddings: string, [width: number], [height: number], [seed: number]) | constructor / setup | compute and draw one deterministic scaled dot-product self-attention head from explicit token embeddings |
attend(attention: id or tag, token: number, [dur: number], [ease: easing]) | state-changing verb | focus one 1-based query token and reveal its Q/K matches, softmax row, weighted V mix, and residual lane |
topk(id: new id, attention: id or tag, token: number, center: (x, y), labels: string, [k: number], [width: number], [height: number], [seed: number]) | constructor / setup | project one attention residual into an authored candidate vocabulary and draw the exact top-k softmax probabilities |
Optics
| API | Kind | What it does |
|---|---|---|
refract(id: new id, [center: (x, y)], [n1: number], [n2: number], [angle: number]) | constructor / setup | a light ray bending across a boundary (Snell’s law); run(id) sweeps the angle, showing total internal reflection |
lens(id: new id, [center: (x, y)], [focal: number], [aperture: number]) | constructor / setup | a converging lens focusing parallel rays to a focal point; run(id) sweeps the focal length so the focus slides |
prism(id: new id, [center: (x, y)], [glass: string]) | constructor / setup | white light splitting into a spectrum through a prism (real Sellmeier dispersion); run(id) sweeps the incidence angle |
achromat(id: new id, [center: (x, y)], [aperture: number]) | constructor / setup | chromatic aberration (red & blue focus apart) and its fix; run(id) sweeps in the achromatic doublet, merging the foci |
lenssystem(id: new id, [center: (x, y)], [preset: string], [object: number]) | constructor / setup | a real multi-element lens ray-traced through its spherical/aspheric surfaces (named preset OR a custom prescription string); draw(id.rays) sketches rays, run(id) sweeps a focus sensor + spot read-out. Optional finite object distance (px). |
rayfan(id: new id, [center: (x, y)], [preset: string]) | constructor / setup | the ray-fan aberration plot of a lens preset (transverse ray error vs pupil height); draw(id.curve) sketches the spherical-aberration S-curve |
spotdiagram(id: new id, [center: (x, y)], [preset: string]) | constructor / setup | the spot diagram of a lens preset at best focus (rays land in a blur disc — spherical aberration), with an RMS read-out and the ideal point focus |
fieldspot(id: new id, [center: (x, y)], [preset: string], [field: number]) | constructor / setup | the off-axis spot diagram (3-D pupil trace at a field angle) — a coma comet / astigmatic blur off-axis, with an Airy-disk diffraction-limit overlay |
Creator
| API | Kind | What it does |
|---|---|---|
creator(id: new id, spec: string) | constructor / setup | a reusable v2 creator profile: handle/platforms plus name, tagline, logo, palette, footer, CTA and safe-area keys |
socials(id: id or tag, [at: (x, y)]) | constructor / setup | draw the creator profile’s responsive footer; social mode uses normalized native YouTube/X/Instagram/TikTok/Facebook/LinkedIn/GitHub/web/email icons |
quiz(id: new id, question: string, [style: string]) | constructor / setup | start a responsive quiz: studio default; optional skin/reveal/layout/density/labels/timer/pace/seconds/motion/safe/accent controls |
option(id: id or tag, text: string, [correct: id or tag]) | constructor / setup | add one of up to six fitted answer cards with stable A–F semantic tags; a trailing correct marks the right one |
timing(…) | constructor / setup | generic named-phase timing controller (optional position) or quiz-specific pace/phase configuration; use generic controllers with timed/during |
timerstyle(…) | constructor / setup | style a quiz or generic timing controller: optional generic position plus native look, number, direction, size, colours, label and finish |
safezone(id: new id, [inset: number]) | constructor / setup | a responsive safe-area guide; optional numeric inset or shorts/reels/tiktok/clean profile |
countdown(id: new id, [at: (x, y)], [secs: number], [style: string]) | constructor / setup | a standalone Timing v2 widget sharing the quiz timer looks and style controls; play with run(id, secs) |
figure(target: id or tag, [center: (x, y)], [size: (x, y)]) | constructor / setup | fit a complete tagged group into the responsive media region, including text/images/equations and live-dependency checks |
explain(quiz: id or tag, text: string, [source: string]) | constructor / setup | attach optional author-supplied answer context and source to a quiz reveal |
endcard(profile: id or tag, [spec: string]) | constructor / setup | build a hidden responsive creator end card; reveal profile.endcard at the final beat |
Fixed language vocabularies
Colours
fg, white, void, bg, black, ink, charcoal, slate, silver, cyan, magenta, pink, accent, lime, green, gold, amber, yellow, red, crimson, orange, blue, azure, teal, turquoise, violet, purple, coral, salmon, indigo, mint, seafoam, dim, gray, grey, panel, rainbow
Easings
linear, smooth, inout, in, out, overshoot, back, bounce, elastic, spring
Named plot functions
sin, cos, tan, asin, arcsin, acos, arccos, atan, arctan, parabola, sq, square, cubic, cube, line, id, identity, abs, exp, sqrt, log, ln, recip, inv, gauss, bell, sinc, sigmoid, logistic, relu, step, heaviside
Canvas presets
16:9, 1080p, 4k, square, portrait, 4:5, 4:3, widescreen, landscape, 720p, fullhd, hd, 2160p, 1:1, 9:16, vertical, story, reel, feed
Templates and aliases
black, mono, plain, terminal, paper, blueprint, shorts, monochrome, blackwhite, black-white, bw, blank, clean, neon, shell, print, light, blue, short, punch
Language keywords
let, for, if, else, def, in, sum, prod, min, max
Reserved variables
w, h, cx, cy, pi, e, tau, inf, infinity
Systems routing and ports
Routing: orthogonal
Ports: auto, left, right, top, bottom
Speech services and curated Cartesia voices
Services: gtts, google, cartesia, elevenlabs, 11labs
Cartesia voices: katie, skylar, jameson, gemma, archie
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 #ff0000.
# ❌ unknown colour `chartreuse`
color(dot, chartreuse);
# ✅ a palette colour…
color(dot, coral);
# ✅ …or a computed one, 0–360
hue(dot, 210);
Palette: cyan magenta lime gold red orange blue teal violet coral indigo mint 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.