Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. The cast — the shapes on screen (a circle, a line, some text). You give each one a name.
  2. 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.

DestinationLinkUse it for
Manic homemaniclang.comProduct overview, downloads, and plans.
Manic Createapp.maniclang.com/createWrite, check, preview, and render a Manic project in the browser.
Examples galleryapp.maniclang.com/galleryBrowse runnable stories and open one in Create.
Manic accountapp.maniclang.com/accountSign in, manage API keys, credits, and billing.
Documentationdocs.maniclang.comLearn the language, CLI, kits, assets, and production workflow.
Interactive API referencedocs.maniclang.com/api/Explore and try the hosted Platform REST API.
Platform API base URLapi.maniclang.comBase URL for API clients: https://api.maniclang.com/v1/....
Chrome extensionManic Animate on Chrome Web StoreTurn selected web-page text, equations, and diagrams into an editable Manic project.
Source and releasesgithub.com/maniclang-x/manicInstallation scripts, release archives, engine source, and issue tracking.
Updates@anish2good on X · r/maniclangFollow 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

CommandPurpose
manic check story.manicParse and validate without opening a window
manic fix story.manicApply safe mechanical source fixes in place
manic story.manicOpen an interactive preview from t=0
manic story.manic --record outExport a final MP4
manic story.manic --still 2.5Export the frame at 2.5 seconds
manic story.manic --gif --record outExport an animated GIF
manic stages story.manicList named stages and their durations
manic voice story.manicReport narration/TTS usage without synthesis
manic svgcheck assets/Screen SVG files for importer compatibility
manic uiOpen the Workbench — a local browser studio for your project
manic versionShow the installed version
manic --helpShow 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-output folder.
  • 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
  • status shows the authoritative online balance when connected and reachable.
  • sync refreshes grants and reconciles completed offline work.
  • logout uses 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(...) plus speak("…") 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

linemeaning
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 / hiddenmodifiers — 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, cy are the canvas centre. manic gives you w, h, cx, cy for 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, then draw, then pulse play one after another. To make things happen at the same time, you wrap them in par { … } — 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.

shapewritedraws
circlecircle(sun, (cx, cy), 90);a circle, radius 90 — filled + outlined by default
rectrect(box, (cx, cy), 200, 120);a rectangle — filled + outlined by default
lineline(edge, (100, 100), (400, 300));a line from point to point (stroke only)
arrowarrow(v, (100, 400), (400, 400));a line with an arrowhead at the end (stroke only)
dotdot(p, (cx, cy), 8);a small filled disc (no outline) — hides crossings
texttext(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:

modifiereffectexample
color(id, c)fill / stroke colourcolor(sun, cyan);
stroke(id, w)line thicknessstroke(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 strokesgradient(wave, blue, cyan, gold, 270);
size(id, n)text sizesize(cap, 30);
glow(id, n)neon halo strengthglow(sun, 8);
opacity(id, 0..1, [layer])transparency; add fill or stroke to set just that layer, independent of the otheropacity(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 filloutline(box, gold);
hue(id, deg)colour by an angle (0–360) — per-entity colour in loopshue(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 thin arc) for angle marks and “regions of interest.” A filled disc 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.

circle defaults to filled + outlined, with a soft slate rim (#9AA6C9) that reads cleanly over axes. For math constructions always pair outlined(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), not dot — 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, and opacity can 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 — while opacity(id, v, stroke) fades just the rim. On a hollow shape (outlined) the fill is off, so colour the visible ring with outline, not color. (Paths — line, arrow, ellipse — are stroke-only: color sets their stroke and there is no fill layer.)

And two that decide how a shape first appears:

modifierpairs withgives
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) use hue(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
▶ Watch: reveal

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.

▶ Watch: motion

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.

▶ Watch: text

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”.

wrapperplays its steps…use for
(nothing)one after anotherthe normal flow
par { … }all at the same instantreveal a group at once
seq { … }one after another (explicit)grouping inside a par
stagger(d) { … }each one d seconds after the lastcascades / waves
step("name") { … }all at once, with a named exported startchange 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:

pieceresponsibility
timingdeclares the phase names, durations, exact total, and optional clock position
timerstylechanges only the visible clock; it never changes scene timing
timedruns the clock and schedules the complete phase contract
duringcontains 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

formuse
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 as show, draw, run, wait, par, and seq inside.
  • A short phase block is padded automatically. A block that exceeds its phase is an error.
  • A phase may have at most one during block. Combine related work inside it with par or seq.
  • 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 call run(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.

lookbest fit
ringneutral default; compact and familiar
barlong processes or wide layouts
segmentsenergetic stages and presentations
ticksprecise, technical, or measurement-led scenes
numberminimal layouts where the value is enough
pulseshort, urgent moments; use sparingly
nonekeep exact phase choreography without showing a clock

The main controls are:

  • number=inside|outside|none and direction=fill|drain
  • size=small|medium|large|0.5..2.0 and thickness=0.4..3.0
  • color, track, label, and font=mono|display
  • finish=fade|hold|flash|pulse for 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 by timed and during.
  • timing(quiz_id, "calm ask=... think=... reveal=...") configures a Creator quiz and is played with run(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 symptomfix
controller id is already in usechoose a fresh id for generic timing
a phase overrunsshorten its sequence, compose simultaneous work with par, or increase the declared phase
unknown phasematch the name in during to the timing declaration
duplicate phase blockcombine the work into one during block
competing durationremove the duration argument from run(clock, dur)
timer appears twiceremove 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:

nameisnameis
cyanelectric bluegoldwarm amber
magentahot pinkredwarm red
limeacid greenorangeorange
bluetrue blue (≠ cyan)tealcyan–green
violettrue purplecoralsoft warm red-orange
indigodeep blue–violetmintsoft cool green
dimmuted grey-violetfgnear-white (default text)
voidthe backgroundpaneldark 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:

easingfeel
linearconstant speed (mechanical)
smoothease in and out (the default, natural)
in / outaccelerate / decelerate
backovershoot slightly and settle
bouncebounce at the end
elastic / springwobble / 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
templatelook
blackdefault — 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
terminalthe 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
shortsrestrained 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

templatebest forcharacter
blackcreator stories, vivid explainers and general authoringexact #000000 canvas, full semantic colour, no chrome
monoprofessional explainers, proofs and restrained Reelsblack-and-white editorial surface, clear luminance hierarchy, subtle glow
plain (alias blank)dark indigo neon scenesoriginal neon semantic colours on a subtly tinted near-black surface
terminalcode, algorithms and technical demosneon terminal frame and stronger chrome
papertextbook figures, worksheets and printwhite page, dark ink, crisp low-glow rendering
blueprintgeometry, engineering and constructioncyan/white drafting marks on deep navy
shortsenergetic social content where hue mattersrestrained 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 plain when you specifically want the original indigo-neon surface.
  • Use paper or blueprint to 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 — write r*x, i*dx, pi*t, never rx / 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:

  1. id, centre, and fitted square size;
  2. the starting string (axiom);
  3. semicolon-separated rewrite rules such as "X=...;Y=...";
  4. 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 iterations one 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.manic for four canonical grammars and creator-lsystem-fractal-curve.manic for 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 separate gapx / 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 URIKindUseful for
asset:manic-logo.pngPNGimage(...), Creator logo=, a Manic-branded example
asset:models/manic-pyramid.objGeometry-only OBJmodel3(...), a beacon, monument, marker, or placeholder model
asset:models/manic-console.objGrouped geometry-only OBJassembly3(...), addressable base/screen/key parts, staged technical stories
asset:svg/lucide/<name>.svgSVG icons — Lucide (ISC)svg(...), themeable line icons (recolour to taste)
asset:svg/heroicons/<name>.svgSVG icons — Heroicons (MIT)svg(...), solid glyphs
asset:svg/flags/<cc>.svgSVG flags — flag-icons (MIT)svg(...), country flags in real colours
asset:svg/emoji/<name-or-codepoint>.svgSVG emoji — Twemoji (CC-BY 4.0)svg(...), flat colour emoji; fire.svg/1f525.svg
asset:svg/heart.svg · pin.svg · robot.svg · map-region.svgSVG demo art (CC0)svg(...), the import demos
asset:svg/physics/effusion-reservoir.svgSVG scientific apparatus (CC0)reusable thermal-reservoir/nozzle shell for process stories
asset:svg/physics/rocket-cutaway.svgSVG scientific apparatus (CC0)cutaway launch vehicle for payload, propellant, combustion, nozzle, and thrust stories
asset:svg/physics/rocket-first-stage.svgSVG scientific apparatus (CC0)independently movable launch booster
asset:svg/physics/rocket-second-stage.svgSVG scientific apparatus (CC0)independently movable second stage
asset:svg/physics/rocket-upper-stage.svgSVG 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 with currentColor, a host-themed placeholder. Manic imports those as themeable geometry: they take the entity’s own colour, so recolor(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

  1. Put it under assets/ in a typed folder such as models/, with a lowercase, descriptive filename.
  2. Add its stable URI to this page and assets/README.md.
  3. Add or update a checked .manic example using the URI.
  4. Keep models geometry-only; do not add scripts, arbitrary shaders, or remote dependencies.
  5. 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
SetCountNamingLicenceImports as
lucide/~1,756icon name (cpu, database, map-pin, …)ISCthemeable outline (recolour it)
heroicons/~324icon name (bolt, beaker, rocket-launch, …)MITthemeable solid glyph
flags/~271ISO 3166-1 alpha-2 (us, in, jp, de, …)MITfull colour
emoji/~4,009Unicode codepoint (1f525.svg) or friendly aliasCC-BY 4.0full 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. svgcheck a 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:

AliasCodepointAliasCodepointAliasCodepoint
grinning1f600heart2764sun2600
smile1f604fire1f525moon1f319
joy1f602rocket1f680cloud2601
wink1f609star2b50zap26a1
thumbsup1f44dtrophy1f3c6rainbow1f308
thumbsdown1f44eparty1f389earth1f30d
check2705brain1f9e0target1f3af
cross274cbulb1f4a1flag1f6a9
warning26a0eyes1f440hourglass231b
thinking1f914clap1f44fchart1f4c8
sob1f62dwave1f44bcomputer1f4bb
muscle1f4aamoney1f4b0phone1f4f1
lock1f512key1f511mail1f4e7
bell1f514door1f6aacar1f697
goat1f410

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.

SetCountLicencePath
Lucide1,756ISCasset:svg/lucide/<name>.svg
Heroicons324MITasset:svg/heroicons/<name>.svg
Flags271MITasset:svg/flags/<name>.svg
Emoji4,073CC-BY 4.0asset:svg/emoji/<name>.svg
Physics5CC0asset: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

steprecommended starting choicewhy
Formatcanvas("9:16")native vertical composition
Surfaceomit the call or use template("black")exact-black full-colour default; use mono for intentional grayscale
Safe areasafe=reels on the creator and quizprotects important content from platform UI
Hierarchystudio layout=media-first density=comfortablequestion first, one focal visual, readable answers
Motionmotion=calm or motion=studiopurposeful movement without visual noise
Timing9–13 seconds for one quiz beatenough time to read, think, and absorb the reveal
Closesignature footer plus one end-card CTAconsistent 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 all pass 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:

  1. Declare the cast once — equation, plot, diagram, labels, caption, profile.
  2. Give each story beat a short, meaningful name.
  3. Inside that step, describe only what changes.
  4. 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:

layerManic vocabularycreator decision
Worldshapes, equations, plots, text, Creator profileWhat should remain recognisable throughout?
Storystep("question"), step("explain"), step("result")What changes in the viewer’s understanding?
Motionrewrite, to, draw, say, show, fadeHow 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 say instead 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 19 to jump to visible stages;
  • press R to 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, and duration;
  • 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, not show-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 wait after 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

partvocabularycreator decision
ExposeparameterWhich value should the viewer follow?
ConnectbindWhich representations explain that value?
Travelstep + 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 all after 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 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:

checkwhat it protects
Canvas boundstext, equations, images, or primary objects outside the frame
Creator safe areacontent underneath platform controls or too close to a protected edge
Content overlaptwo substantial text, equation, or image boxes competing for the same space
Readabilitytext 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, and cy instead 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|clean profile. Use safezone(...) 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 intentManicResult
Keep one object beside anotherattach(child, target, [(dx,dy)])the child follows every resolved target position
Stop followingattach(child, none)the child releases at the settled position without a second verb
Change visual identitybecome(source, blueprint, [dur], [ease])the source keeps its id and settles exactly on the blueprint
Turn a whole arrangementturn(id_or_tag, pivot, degrees, [dur], [ease])every member follows the same circular pivot motion
Move a real object on a pathtravel(object, path, dur, ease)the object arrives and remains at the endpoint
Send temporary or sustained path emphasis`flow(path, dur, [forwardreverse
Add contained ambient lifewander(particles, dur)seeded, repeatable motion within the container
Reorganize the same particles`arrange(particles, region, “randomgrid
Progressively move a collectionstream(particles, path, dur, spread, ease)seeded persistent items leave in a natural order and retain arrival/speed measurements
Release particles with individual speedsemit(particles, path, uniform|maxwell, dur, spread, ease)every persistent item receives a deterministic sampled speed; faster items traverse sooner
Follow a visual fieldadvect(particles, vectorfield, dur, rate)every persistent item follows the same bounded formula field with seekable deterministic motion
Route a collection through choicesbranch(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 measurementcollect(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 measuredobserve(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 propertyto(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:

  1. A question travels while its label stays attached; a path pulse guides the eye and surrounding facts wander.
  2. The notation rewrites, the same question becomes a model, and the same facts arrange into a system. The model then spins locally.
  3. 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 par for changes that belong to one idea and seq for cause-and-effect.
  • Prefer smooth for explanatory transformations and out for a short settle.
  • Keep an important object id alive instead of fading it out and rebuilding it.
  • Leave a final wait so the audience can read the state motion created.
  • Preview by named step and 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:

  1. Pick a voice once — voice("…")
  2. Speak full sentences — speak("…")
  3. 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

sayspeak
What it doesChanges on-screen textSpeaks a voice-over line
Heard?NoYes
Needs voice(...)?NoYes — once per file
Best forShort punch lines, labelsFull sentences the viewer hears
Examplesay(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?)
voice("gtts");                         // simple built-in
voice("cartesia");                     // Skylar
voice("cartesia", "katie");
voice("cartesia", "jameson");
voice("elevenlabs");                   // Roger
voice("elevenlabs", "alice");
voice("elevenlabs", "jessica");
serviceeasy speaker names
Cartesiakatie, skylar, jameson, gemma, archie
ElevenLabsroger, 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 lengthBudget 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 speak won’t stall forever, but split big paragraphs into separate lines anyway; they read better.
  • wait(...) adds on top of the spoken time. Since speak already 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 --still preview 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 θ).");


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 / speaksay alone 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.

LeverBuiltinsBuys you
Stage the partshidden(id.part), untraced(id.path), then show/drawreveal 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
Typewritertype(id,dur) + cursor(id) (set the string with say(id,"…",0.1) first)a lab-note / terminal feel that types itself out
Live datacounter(id,(x,y),start,decimals,"pre","suf") + to(id, value, target, dur)a number that ticks up (k, period, acceleration)
Cameracam((x,y),dur,ease) + zoom(factor,dur,ease)push in on a part, then pull back — cinematography
Pin a HUDsticky(id)keep a caption / counter fixed on screen while the camera cam/zooms the world
Kinetic typecaption(id,"…",(x,y),size,color) + wordpop(id,dur) / karaoke(id,dur,color)words pop in, or a highlight sweeps across them
Name the partstext, leader arrow, bracelabel, reference linesay what each thing is
Emphasizeflash(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 lifeparticles(id, circle_or_rect, count, radius, seed) + wander(id,dur)contained bubbles, dust, stars, data, or molecules without hand-animating dots
Change a particle stateparticles(...,"grid") + `arrange(id,container,“randomgrid
Show a transferlink(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 pathtravel(marker,path,dur,ease)a plot marker, vehicle, token, or probe follows the authored path once and stops at its endpoint
Preserve identitywatermark(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
Broadcastflash(id.parts, lime)hit every part of the sim at once
State the lawa text with the formulatie the motion to the equation
Multiple readingsreveal well / phase / timegraph / energygraph, then runone motion, several synchronized views
Declutterfade(...) the static annotationsclean 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/caption updated with say, or let the typewriter / camera / kinetic-type do the pacing. Vary the lens per scene — if every lesson uses the identical section+say+show loop, the medium looks one-note; the point of the kit above is that manic has range.

Reveal-order gotchas

  • show/fade force 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, draw it (traces the stroke, leaves opacity alone) or just display it faint from the start — don’t show it.
  • draw is the nicest reveal for strokes — lines, arrows, curves, and paths sketch themselves on. Set the entity untraced first, then draw(id, dur).
  • fade the 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: a typed lab-note with a cursor, and counters that tick k and the period up, over Hooke’s law → parabolic well → SHM.
  • The Atwood Machine (pulley-annotated) — camera: cam + zoom push in on the masses for the imbalance beat, then pull back to release; the caption + counter are sticky, so they stay pinned through the zoom.
  • Brachistochrone race (brachistochrone-annotated) — kinetic typography on a multi-body sim: wordpop the question, karaoke the 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 filesmolecule3 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 skinstudio (rounded editorial default) · badge (framed panel + coloured letter badges) · minimal (kicker + accent rule, outline rows) · glass (glowing borders) · plain (flat) — and a question revealtype (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.

StoryStart withAnimate with
Scalar → vector → matrix → tensortensorcore Manic verbs
One activation functionactivationcore Manic verbs
Prediction through a dense modelnetworkforward
Why a prediction is wrongnetwork + forwardloss
How gradients assign creditprediction + lossbackward
One visible learning correctioncompleted backward passupdate
Image → feature maptensor + kernel + convolvescan
Smaller feature maptensor + poolscan
Text → tokens → positioned vectorstokenize + embeddingcore Manic verbs
How one token finds contextattentionattend
Context → candidate probabilitiesattention + topkcore Manic verbs
Complete transformer blocktransformerencode
Hidden state → logits → next tokentransformer + logitssample

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

KindWordsWhat the creator supplies
Figurestensor, kernel, activation, network, tokenize, embedding, attention, transformervalues, shape, text, boundaries, activation names, embeddings, compact block options, or a deterministic seed
Derived viewsconvolve, pool, topk, logitsa source figure plus operator, candidate, token, temperature, or projection choices
Computationforward, loss, backward, update, scan, attend, encode, sampleinput, 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.
  • 21 is the deterministic seed. The same file produces the same weights, activations, prediction, and frames every time.
  • The input supplied to forward must 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:

  1. forward stores the real activations for the authored input.
  2. loss compares the output with the target. crossentropy requires a softmax output and a non-negative target distribution that sums to one. mse supports other output activations and arbitrary finite targets.
  3. backward calculates exact reverse-mode gradients for every visible and hidden parameter. The pulse travels output → input along the existing edges.
  4. update applies parameter -= 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_model must divide exactly across them;
  • mask=none|causal; causal future cells receive zero probability;
  • mlp=WIDTH up to 32 and activation=gelu|relu|silu|tanh;
  • norm=pre|post, which changes where both layer normalizations happen;
  • dropout=0..less-than-1, mode=inference|training, and a reproducible seed.

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 stringExact 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.
  • loss places the target beside each output without replacing the prediction. Error magnitude focuses attention on the outputs that disagree.
  • backward temporarily recolours connections by gradient sign and weights their emphasis by gradient magnitude. The settled network remains the same object, ready for the update.
  • update shows 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.
  • restore sends 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:

TagSelects
netthe complete network figure
net.nodes, net.edges, net.valuesone visual role
net.layer0, net.layer1, …one layer
net.input, net.hidden, net.outputsemantic layer groups
net.probabilitiesoutput bars and readouts
net.losssupervised target readouts
image.cells, image.values, image.labelstensor visual roles
image.channel0, image.row0, image.col0tensor slices
image.c0.r0c0 and .valueone cell and its numeric text
feature.scanreceptive-field/operator/destination overlays
words.source, .tokens, .indices, .tokenNtokenization stages
context.vectors, .positions, .combinedembedding addition stages
context.rowN, .dimN, .operatorsone token, feature, or operator
block.heads, .headN, .mask, .matrixmulti-head attention and masks
block.concat, .projection, .residual1, .norm1first half of the block
block.mlp, .activation, .dropout, .residual2, .norm2second half
block.input, .output, .tokenN, .rowNpersistent token lanes
head.tokens, .q, .k, .v, .matrixattention stages
head.connections, .outputs, .residualattention flow and residual lanes
next.labels, .bars, .probabilitiestop-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:

  1. Declare a chart — racechart(id, layout, periods)
  2. Paste the data — racedata(id, "…")
  3. 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 writeResolves to
usa flag shorthand → asset:svg/flags/us.svg
rocket, heart, trophyan emoji / icon alias
asset:svg/lucide/lightbulb.svgan explicit set path
logos/acme.svgyour 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), or raceline(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

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 bounded key=value string, below.
keyvaluesmeaning
styleball (default) · sticks · wireframe · spacefillhow it is drawn
hydrogens1 (default) · 00 drops them and their bonds
spindegrees per second, default 240 holds it still
axisz (default) · x · ywhich axis the turn is about
turn / tiltdegreesa static orientation, applied once
record1 (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 seewhat justifies it
a carbon is a bare vertexthe element is C
hydrogens on carbon vanishthey are bonded to a C
an oxygen reads OHit has one explicit H neighbour
a nitrogen reads NH2it has two
a double bond leans into the ringthe crowded side, from the neighbours’ own coordinates — no ring detection needed
a bond is a solid wedgethe molfile’s fourth bond column says 1
a lone carbon reads CH4a 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.

keyvaluesmeaning
carbons0 (default) · 1letter every carbon too — the explicit style a first lesson uses
hydrogens0 (default) · 1draw them as their own vertices instead of folding them in
labelstext (default) · latexhow the letters are set
colorany palette name or #rrggbbthe bond and carbon colour
record1 (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 getthe rule behind it
a chain zig-zagseach bond turns 60° from the last, so the interior angle is the tetrahedral 120°
a ring is a regular polygonclosed on its own circumcircle, whatever its size
a fused ring reflects across the shared bondso naphthalene reads as naphthalene, not as two rings on top of each other
a substituent points out of its ringradially, away from the ring centre — toluene’s methyl never points inward
an aromatic ring gets real double bondsthe 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:

sourceavailable for
{id}.iupacthe record’s own name, or generatedbundled files, your files, and SMILES within a narrow scope
{id}.namethe curated asset:molecules/ filenamebundled 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:

hydroxylcarbonylcarboxylaldehyde
ketoneesteramideprimary_amine
secondary_aminetertiary_aminenitronitrile
thiolsulfidesulfoxidesulfone
halogenetherepoxidearomatic

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:

addresswhat 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}.bondsthe two groups
{id}.multibondsonly 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:

  1. 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.
  2. 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.
  3. 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:

  1. Type the circuit — circuit(id, center, netlist)
  2. Play it — run(id, seconds)
  3. Take something outcut(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:

partpins
transistors, opamp, gates, comparator, vco, ideal-switch, spdt-switchthird pin one step below the second coordinate
flip-flopstwo outputs, one and two steps below the second coordinate
relaythird below the second, fourth below the first
transmission-line, dependent sourcesoutput 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
  • speed multiplies 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.
  • shape is circle (default), square or diamond.
  • color takes 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:

addresswhat it is
{id}.c{k}the k-th component, in netlist order
{id}.<name>a component you gave name=X
{id}.partsevery component (stroke batch)
{id}.labelsthe value labels
{id}.nodes · {id}.n{node}junction dots
{id}.chargethe charge dots
{id}.glowlamp/LED halos
{id}.probes · {id}.scopesreadouts
{id}.<type>severy 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:

grouptypes
passive + sourceswire ground resistor capacitor polarized-cap inductor dc-voltage ac-voltage dc-current clock lamp
switchingswitch push-switch spdt-switch ideal-switch relay fuse
junctionsdiode led zener
transistorsbjt-npn bjt-pnp mosfet-n mosfet-p jfet-n jfet-p darlington-npn darlington-pnp
analogueopamp comparator schmitt vco transmission-line
combinational logicand-gate or-gate nand-gate nor-gate xor-gate not-gate logic-input logic-output
sequential logicd-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

LayerWordsMeaning
Ownershiparchitecture, cluster, nodewhat belongs where
Topologyconnect, linkpossible directed paths and neutral visual relationships
Runtime storymessage, route, hotpathwhat one persistent message actually does
Aggregate activityflowuntracked 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.

#PatternHow to author it
1One-way — one source, one destinationconnect(a2b, a, b)
2Round-trip — request and response as separate, honest lanesconnect(fwd, a, b) + connect(ret, b, a) (style/colour them differently)
3Orthogonal — right-angle Manhattan routing, one identityconnect(a2b, a, b, orthogonal)
4Vertical ports — the lane enters/leaves a chosen faceconnect(a2b, a, b, orthogonal, top, bottom)
5Fan-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
6Diagonal duplex — two directions with distinct stylingtwo 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:

MingrammerManic
Diagram(...)architecture(...)
Cluster(...)cluster(...)
ECS(...), RDS(...), and other provider objectsnode(..., "aws:kind", ...)
a >> bconnect(path, a, b)
a - blink(path, a.card, b.card)
runtime behaviorexplicit 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:

AliasResolves to
aws:apigatewayaws:api-gateway
aws:route53aws:route-53
aws:elbaws:elastic-load-balancing
aws:load-balanceraws:elastic-load-balancing
aws:sqsaws:simple-queue-service-sqs
aws:s3aws:simple-storage-service-s3-bucket
aws:ecsaws:elastic-container-service
aws:eksaws: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 3cube3, 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]");
  • implicit3 samples a bounded scalar field and extracts one isosurface.
  • collection3data and links3data retain explicit coordinates and arbitrary edges as two batches—useful for Cayley diagrams, dependency graphs, and imported scientific datasets.
  • lsystem3 is a 3D turtle grammar: F draws, f moves, +/- 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:

wordcreator 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. view3 and turn3 both 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 choose front, side, top, or isometric. A margin around 1.15–1.35 is useful for landscape; start around 1.45–1.7 for 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.
  • view3 uses 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.
  • travel3 reads the route’s current transform every frame. A route may rotate or move in the same par block 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:

needoption
data boundsx=-2..4 y=0..8 z=0.1..100
one or more wallsplanes=xy:min,xz:min,yz:min
a top or central planeplanes=xy:max,xz:origin
an exact sectionplanes=xy:-0.5
two coloured parallel sectionsplanes=xy:-0.5@cyan,xy:1.5@magenta
shared intervalsmajor=1 minor=0.5
independent intervalsxmajor=0.5 ymajor=1 zmajor=2
logarithmic datazscale=log (the corresponding range must be positive)
page or spatial stylingmode=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

shapewritedraws
cube3cube3(box, (0,0,1), (2,2,2));a box (width, depth, height)
sphere3sphere3(ball, (0,0,1), 0.9);a ball of that radius
point3point3(p, (1,1,1));a small marker in space
line3 / arrow3arrow3(v, (0,0,0), (0,0,2));a segment / a vector
grid3grid3(floor, (0,0,0), 5, 1);a ground grid to sit things on
axes3axes3(ax, (0,0,0), 3);labelled x, y, z arrows
frame3frame3(world,(0,0,0),(8,8,6),"planes=xy:min,xz:min mode=textbook");bounded scientific axes plus selected grid planes
randomwalk3randomwalk3(walk,(0,0,0),10000,21,"mode=turtle angle=60 color=turn shade=depth");one deterministic 3D path with a local/world direction model
hilbert3hilbert3(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=axis chooses one of ±x, ±y, ±z on every step.
  • mode=turtle advances along a local heading, then turns that orientation frame; use angle=90 or 60 for the classic textbook constructions.
  • distribution=uniform|gaussian changes how the six choices are sampled.
  • color=direction (or turn) paints segments by the selected choice.
  • shade=depth adds a camera-aware far/near cue without changing the path.
  • scale is the world-space length of every step. seed makes 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.

IntentManic words
many stable pointscollection3
relationships among themlinks3, child3
one dependent articulated chainchain3, trail3
a changing 3D field and its motionvectorfield3, advect3
a camera that follows one memberfollowshot3
named parts inside one OBJassembly3
a small offline sound beatcue

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.

StoryDimensional journeyTeaching idea
The trapped light beam1D → 2D → 3DDistance grows from 5 to 13 to 85 through nested right triangles.
How space learned to growpoint → line → plane → roomA line sweeps into a surface; the surface lifts into volume.
Length, area, volume1D → 2D → 3DWhy measured units become cm, cm², and cm³.
A point gets an addressx → (x,y) → (x,y,z)Each new axis adds one coordinate to the same point.
The revolving semicirclediameter → curve → sphereA solid can be generated from a lower-dimensional rule.
Statistical dimensionslist → scatter → cloudMore variables require a richer coordinate world.
Dimension reduction3D → 2D → 1DA sphere reveals a section, then the section reveals a diameter.
Watermelon sectionswhole → 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. Write pi*t, never pit (manic reads pit as one unknown word). Same for v*v, not vv.

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 bind when one curve/surface remains the subject and its mathematical rule changes continuously;
  • use morph3 when 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 paper template 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 profile r(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=smooth for spheres/organic surfaces and leave boxes flat.
  • A little mesh clarifies topology; 1 is intentionally strong.
  • depth and shadow are subtle readability controls from 0 to 1, not a replacement for authored lighting.
  • checker and stripes are 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 URIWhat it isGood for
asset:models/manic-pyramid.objSmall generic pyramid OBJLearning model3, a beacon, marker, monument, or placeholder model
asset:models/manic-console.objGrouped 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 point
  • rotate3 — turn it (degrees around x, y, z)
  • grow3 — stretch a line or arrow’s tip to a new point
  • orbit3 — swing the camera (angle around, angle up, distance)
  • roll3 — bank the camera around its viewing direction; it can run beside orbit3 in par, including through stable overhead/underside views
  • look3 — 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…
huecolor with a palette name
strokethick
move / rotate / spinmove3 / rotate3
cam / zoomcamera3 / orbit3
morphmorph3

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 DSLshader / raymarchRaw GLSLglsl
you writemanic formulas (let r = …)a real GLSL fragment shader
best forfields, SDF art, ray-marched scenes you want in the scenepasting an existing Shadertoy, or exotic multi-pass tricks
integrates withparameters, camera3, the timeline, compositing, the editor’s autocomplete/checksthe universal iTime/iResolution/iMouse (plus opt-in uniforms)
runs onGPU and a deterministic CPU fallbackGPU (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) + optional sat / val (HSL — great with atan2),
  • 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/v are 0..1 on both axes, so a raw length(u-0.5, v-0.5) is an ellipse on a wide canvas. Correct with asp (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 effectglsl(). 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() supplies iTime / iResolution / iMouse (and the iCam* basis with camera3) — but no iChannel0..3 textures/audio/video/buffers. A paste that reads texture(iChannel0, …) won’t compile. When the channel was only a noise source, swap it for a procedural hash: texture(iChannel0, vec2(k)).xfract(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 itplot, 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 ValueTracker feeding an updater feeding an always_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

ManimManicNotes
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 labelsxtick(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.
SpiralInspiralin(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 / Swapcycle(a, b, c, …, [dur], [arc°])Each entity moves to the next’s position along an arc (default 90°).
Clockwise / CounterclockwiseTransformcycle(…, arc°) (signed arc)Points travel an oriented arc, set by the arc-degrees argument.
Line(...).rotate(theta) about a vertexturn(point, vertex, degrees, dur)Rotate a point about a pivot; attached arms/arcs follow.
Angle(l1, l2) + add_updateranglemark(id, a, b, c, ["θ"])Reactive arc; optional label rides the bisector as the angle opens.
Indicate / Flashpulse(id) / flash(id)Attention: a scale bump / a radial burst.
Wigglewiggle(id)Scale-pulse + oscillating rotation — the rotate cousin of shake.
Circumscribe / CircleIndicatecircumscribe(id, [color])A temporary outlined box drawn round the target, then faded — works on a group (tag) too.
FocusOnspotlight(id)A translucent disc converges onto the target, then fades.
Blinkblink(id)Blink out-and-in twice.
ShowPassingFlash / FlashAroundpassflash(id, [color])A luminous sliver travels the entity’s outline (closed shapes → light circles it).
x = ValueTracker(0) + set_x updaterslidex(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 / ComplexHomotopydeform(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 / GrowFromPointhidden(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:

  1. Set the frame. Replace the whole Axes(...) block with one coords(...) — ranges, per-axis scale, tips, ticks, numbers, and axis names all live there. Point your plots at the same origin, sx, sy and they line up.
  2. Delete the reactive rig. Every ValueTracker, add_updater, and always_redraw has 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.
  3. Sequence with wait and par. self.play(a, b) becomes par { a; b; }; self.wait(t) becomes wait(t). Reveals are show/draw; hides are fade.
  4. Declare-and-hidden up top. Anything that shouldn’t be on screen at the start is constructed and hidden before the timeline, then shown 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 ManimA Manic kit gives you
a NetworkMobject of circles + edges, activations fakednetwork + 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 neuronsfeed(net, digit) — flattens a tensor/digit into the inputs and traces the lit strokes in
a Surface + hand-animated gradient arrowssurface3 + slice3 (the partial-derivative cross-section) + gradient3 + descend3 (a ball rolling down −∇f, with its trail)
a rack of NumberLines for a high-dimensional pointsliders + setsliders — N coordinate dials with a live Σxᵢ²
a physics sim animated by handthe 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

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

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 (linsolverref) 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 , 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, Q3if 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);

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 parameter draws 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-spiral shader vortex 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/sdroundboxsmin- 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);

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);

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 C4c4(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

  1. Create a scoped key from Manic Account.
  2. Use a mk_test_… key while integrating; use mk_live_… only for production.
  3. Send it as Authorization: Bearer mk_live_… (or X-API-Key).
  4. For render and AI creation requests, always generate an Idempotency-Key and 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 id is the name created by a constructor.
  • id or tag targets 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

APIKindWhat it does
circle(id: new id, center: (x, y), r: number)constructor / setupa circle
rect(id: new id, center: (x, y), w: number, h: number)constructor / setupa rectangle
image(id: new id, center: (x, y), path: string, [w: number], [h: number])constructor / setupa 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 / setupimport 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 / setuptypeset 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 / setupmathparts(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 / setupan 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 verbmove 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 / setupa 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 / setupa straight or bent edge that follows two moving entities
arrow(id: new id, from: (x, y), to: (x, y), [bend: number])constructor / setupan 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 / setupa filled polygon: polygon(id, (x1,y1), (x2,y2), (x3,y3), …, [color])
dot(id: new id, at: (x, y), [r: number])constructor / setupa small filled dot
cloud(…)constructor / setupa point field: N points placed by closed-form formulas of index i and time t
shader(…)constructor / setupa per-pixel colour field: r/g/b (or c) as closed-form formulas of pixel coords u/v and time t
raymarch(…)constructor / setupa 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 / setuprun 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 / setupdeterministic small dots inside a circle or rectangle; optional random/grid/ring layout
text(id: new id, at: (x, y), text: string, [size: number])constructor / setupa text label; optional trailing point size (default 28)
label(…)constructor / setupa label pinned to an entity
caption(id: new id, words: string, at: (x, y), [size: number], [color: colour])constructor / setupa row of caption words (karaoke/pop)
support(id: new id, center: (x, y), [len: number], [dir: string])constructor / setupa 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 / setupa live numeric readout
parameter(id: new id, at: (x, y), initial: number, min: number, max: number, [label: string], [decimals: number])constructor / setupa 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 / setupconnect a parameter to a property, plot/field formula, or stable generated 3-D family
cursor(id: id or tag)constructor / setupgive a text entity a typewriter cursor
sticky(id: id or tag)constructor / setuppin an entity to the screen so it stays fixed through cam/zoom (HUD overlay)
morph(…)constructor / setupsampled-point shape morph a->b
invertpath(id: new id, source: id or tag, center: id or tag, radius: number, [samples: number])constructor / setuplive whole-outline inversion about a named center
reflectpath(id: new id, source: id or tag, mirror: id or tag, [samples: number])constructor / setuplive whole-outline reflection across a named line
copy(new: new id, src: id or tag)constructor / setupduplicate an entity
color(id: id or tag, color: colour)constructor / setupset fill/stroke colour
outline(id: id or tag, color: colour)constructor / setupset outline colour
size(id: id or tag, size: number)constructor / setupset text size
wrap(id: id or tag, width: number)constructor / setupwrap 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 / setupwhich 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 / setupline 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 / setupstack 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 / setupset stroke width
dashed(id: id or tag, [dash: number], [gap: number])constructor / setupuse a repeating dash/gap stroke on a path-like entity
gradient(…)constructor / setupmulti-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 / setupset neon glow amount
plate(id: id or tag, [opacity: number])constructor / setupgive 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 / setupset 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 / setupdrive colour by an HSL hue
rot(id: id or tag, degrees: number)constructor / setupset rotation (degrees)
z(id: id or tag, z: number)constructor / setupset draw order
clip(id: id or tag, region: id or tag)constructor / setupclip 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 / setupclip 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 / setupadd a group tag
bold(id: id or tag)constructor / setupuse the bold mono font
display(id: id or tag)constructor / setupmark visible
hidden(id: id or tag, [from: id or tag])constructor / setupstart 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 / setupSOLID posture: fill on, outline off — the default for circle/rect/polygon. Reverts a hollow shape
outlined(id: id or tag)constructor / setupHOLLOW 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 / setupstart undrawn, ready for draw-on
union(…)constructor / setupboolean union of two shapes
intersect(…)constructor / setupboolean intersection
intersection(…)constructor / setupboolean intersection
difference(…)constructor / setupboolean difference a-b
subtract(…)constructor / setupboolean difference a-b
exclusion(…)constructor / setupboolean symmetric difference
xor(…)constructor / setupboolean symmetric difference
brace(id: new id, p1: (x, y), p2: (x, y), [depth: number], [direction: id or tag])constructor / setupa 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 / setupa brace with a label
bracetext(…)constructor / setupa brace’s text label
draw(id: id or tag, [dur: number], [ease: easing])animation verbtrace a stroke on
flow(path: id or tag, [dur: number], [direction: id or tag], [mode: id or tag])animation verbsend a directional luminous pulse or finite continuous stream over a path
erase(id: id or tag, [dur: number], [ease: easing])animation verbreverse of draw
show(id: id or tag, [dur: number], [ease: easing])animation verbreveal 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 verbfade out
flash(id: id or tag, [color: colour], [dur: number], [ease: easing])animation verbflash a colour, then restore
recolor(id: id or tag, color: colour, [dur: number])animation verbpermanently change colour
pulse(id: id or tag, [dur: number])animation verbgrow-and-settle attention pulse
oscillate(id: id or tag, prop: new id, period: number, amp: number, [phase: number], [dur: number])animation verbcontinuously 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 verbsize 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 verbhorizontal shake (error gesture)
spin(id: id or tag, [degrees: number], [dur: number])animation verbspin about the centre
move(id: id or tag, to: (x, y), [dur: number], [ease: easing])animation verbmove to an absolute point
wander(particles: id or tag, [dur: number])animation verbgently move a particle group inside its source container
burst(particles: id or tag, [dur: number])animation verbexplode 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 verbmove 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 verbmove 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 verbkeep 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 verbtransform 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 verbrotate one entity or tagged arrangement rigidly around a shared pivot
disintegrate(entity: id or tag, [dur: number])state-changing verbdissolve an entity’s actual silhouette into deterministic seekable particles
shift(id: id or tag, by: (x, y), [dur: number], [ease: easing])animation verbmove by a delta
slidex(id: id or tag, x: number, [dur: number], [ease: easing])animation verbslide to an absolute x, keeping y where it is
slidey(id: id or tag, y: number, [dur: number], [ease: easing])animation verbslide 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 verbshift 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 verbscale 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 / setupa 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 verbanimate 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 verbcontinuous 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 verban 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 verbblink an entity out-and-in twice over dur to draw the eye
circumscribe(id: id or tag, [color: colour], [dur: number])state-changing verbdraw 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 verba 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 verba 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 / setupsnapshot 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 verbreveal 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 / setupa 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 / setupa 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 / setupfill + 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 / setupa 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 / setupthe 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 verbanimate a line/arrow endpoint
scale(id: id or tag, factor: number, [dur: number])animation verbanimate scale to a factor
rotate(id: id or tag, degrees: number, [dur: number], [ease: easing])animation verbanimate rotation
say(id: id or tag, text: string, [dur: number], [ease: easing])animation verbcrossfade text to new content
speak(…)animation verbnarrate 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 verbplay a deterministic procedural sound beat: tick, pop, whoosh, or chime
rewrite(equation: id or tag, latex: string, [dur: number], [ease: easing])state-changing verbsmoothly 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 verbtypewriter reveal
to(id: id or tag, prop: new id, value: number, [dur: number], [ease: easing])animation verbanimate any property to a value
set(id: id or tag, prop: new id, value: number, [dur: number], [ease: easing])animation verbalias of to
transform(id: id or tag, origin: (x, y), a: number, b: number, c: number, d: number, [dur: number], [ease: easing])animation verbapply a 2x2 matrix (ApplyMatrix)
swap(a: id or tag, b: id or tag, [dur: number])state-changing verbswap two entities, or array slots i,j
cycle(…)state-changing verbmove entities cyclically into one another’s positions along an optional arc
cam(to: (x, y), [dur: number], [ease: easing])animation verbpan the camera to a point
zoom(factor: number, [dur: number], [ease: easing])animation verbzoom the camera
followshot(target: id or tag, [offset: (x, y)])state-changing verba 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 verbhighlight caption words in sequence
wordpop(…)animation verbpop 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 / setupa 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

APIKindWhat it does
lsystem(id: new id, center: (x, y), size: number, axiom: string, rules: string, [options: string])constructor / setupa 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 / setuprepeat 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 / setupa 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 / setupa batched escape-time Mandelbrot field
polarpath(id: new id, center: (x, y), scale: number, formula: string, range: (x, y), [samples: number], [closed: number])constructor / setupa sampled polar formula path
hull2(id: new id, cloud: id or tag, [depth: number], [pivot: number])constructor / setupan onion-depth hull derived from a batched point cloud

3D

APIKindWhat it does
cloud3(…)constructor / setupa 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 / setupa 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 / setupa point in 3D space
line3(id: new id, from: (x, y, z), to: (x, y, z))constructor / setupa 3D line segment
arrow3(id: new id, from: (x, y, z), to: (x, y, z))constructor / setupa 3D vector arrow
cross3(id: new id, origin: (x, y, z), v: (x, y, z), w: (x, y, z), [color: colour])constructor / setupthe 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 / setupa 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 / setupa 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 / setupthe real eigenvector directions of a 3x3 matrix (invariant lines)
sphere3(id: new id, center: (x, y, z), radius: number)constructor / setupa 3D sphere
collection3(id: new id, center: (x, y, z), count: number, spread: (x, y, z), seed: number, [radius: number])constructor / setuprenderer-batched repeated 3D points with stable indexed children
collection3data(id: new id, center: (x, y, z), points: string, [radius: number])constructor / setuprenderer-batched explicit 3D coordinates with stable indexed children
links3(id: new id, collection: id or tag, mode: id or tag, [neighbors: number])constructor / setuplive relationship mesh over a collection3: chain, nearest, or all
links3data(id: new id, collection: id or tag, edges: string)constructor / setupan explicit arbitrary edge list that follows a 3D collection
trail3(id: new id, collection: id or tag, [child: number], [thickness: number])constructor / setuplive history path for one stable indexed child of a collection3
ring3(id: new id, collection: id or tag, child: number, [segments: number])constructor / setuplive 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 / setupscreen-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 / setupworld-space 3D component history derived from one compiled collection3 child
child3(id: new id, collection: id or tag, index: number, [radius: number])constructor / setupaddressable 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 / setupseeded 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 / setupbounded batched 3D vector field from x/y/z/p component formulas
grid3(id: new id, center: (x, y, z), half: number, [spacing: number])constructor / setupan XY ground grid
axes3(id: new id, origin: (x, y, z), length: number, [step: number])constructor / setupthree colored coordinate axes with ticks + numbers
frame3(id: new id, center: (x, y, z), size: (x, y, z), [options: string])constructor / setupbounded scientific axes and selectable XY/XZ/YZ grid planes
randomwalk3(id: new id, center: (x, y, z), steps: number, [seed: number], [options: string])constructor / setupa 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 / setupa 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 verbrestyle one frame3 as textbook linework or spatial color
move3(id: id or tag, to: (x, y, z), [dur: number], [ease: easing])animation verbmove a 3D entity
shift3(id: id or tag, by: (x, y, z), [dur: number], [ease: easing])animation verbshift a 3D entity
rotate3(id: id or tag, degrees: (x, y, z), [dur: number], [ease: easing])animation verbrotate to Euler angles
grow3(id: id or tag, to: (x, y, z), [dur: number], [ease: easing])animation verbretarget a 3D line or arrow
orbit3(azimuth: number, elevation: number, radius: number, [dur: number], [ease: easing])animation verborbit the 3D camera
roll3(degrees: number, [dur: number], [ease: easing])animation verbroll the 3D camera around its viewing direction
look3(target: (x, y, z), [dur: number], [ease: easing])animation verbmove the 3D camera target
drift3(collection: id or tag, dur: number, [amount: number])state-changing verbcompile deterministic bounded ambient motion for a collection3
chain3(collection: id or tag, lengths: string, rates: string, dur: number)state-changing verbanimate 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 verbadvect a collection3 through a time-varying vectorfield3 with deterministic RK4
followshot3(target: id or tag, [offset: (x, y, z)])state-changing verbmake 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 verbsmoothly 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 verbmove 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 verbattach 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 verbtransform 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 verbturn 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 / setupglue 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 / setuptrack another 3D entity + offset each frame
midpoint3(id: new id, a: id or tag, b: id or tag)constructor / setupa point at the midpoint of two 3D entities
link3(id: new id, a: id or tag, b: id or tag, [trim: number])constructor / setupa live 3D edge between two moving entities
project3(id: new id, source: id or tag, plane: string)constructor / setuplive orthogonal projection of a 3D entity onto xy, xz, or yz
projectpath3(id: new id, source: id or tag, plane: id or tag)constructor / setuplive orthogonal projection of a complete changing 3D path
contour3(id: new id, surface: id or tag, level: number)constructor / setupa level curve on a surface3 height field
label3(label: id or tag, target: id or tag, [world_height: number])constructor / setupprojected label with optional world-space height
curve3(id: new id, x: string, y: string, z: string, [domain: (x, y)])constructor / setupparametric 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 / setupintegrate 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 / setupfilled, 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 / setupDOMAIN 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 / setupan 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 / setupa bounded deterministic branching 3D turtle grammar
heightmap3(id: new id, grid: id or tag, z: string, [size: number])constructor / setuplift 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 / setupsteepest-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 / setupthe 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 / setupgradient 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 / setupthe plane tangent to a surface3 at (x,y)
volume3(id: new id, surface: new id, [res: number], [color: colour])constructor / setupthe 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 / setupparametric 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 / setupcut 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 / setupregular n-gon prism (many sides ≈ cylinder)
pyramid3(id: new id, center: (x, y, z), sides: number, radius: number, height: number)constructor / setupregular n-gon pyramid (many sides ≈ cone)
revolve3(id: new id, center: (x, y, z), profile: string, tdom: (x, y), [sides: number])constructor / setupsolid of revolution from a radius profile r(t)
extrude3(id: new id, source: id or tag, height: number, [center: (x, y, z)])constructor / setupextrude a 2D shape or boolean region into a solid prism (CSG)
morph3(a: id or tag, b: id or tag, [spin: number])constructor / setupset 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 / setuprender a 3D curve3/line3/arrow3 as a tube of the given world radius
finish3(id: id or tag, spec: string)constructor / setupbounded 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 / setupload 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 / setupload 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 / setupvariable-radius tube around a line3, arrow3, or curve3

Math

APIKindWhat it does
colorwheel(id: new id, center: (x, y), radius: number)constructor / setupthe 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 / setupa 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 / setupthe 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 / setupdomain 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 / setupa 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 / setupa 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 / setupa 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 / setupa tick + label on a coords frame’s y-axis at an axis VALUE (see xtick)
field(id: new id, formula: string)constructor / setupdefine 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 / setupy = f(x) as a curve
param(id: new id, center: (x, y), sx: number, sy: number, x: string, y: string, [tdomain: number])constructor / setupa 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 / setupa 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 / setupthe normal (perpendicular) line to a curve at x
slope(id: new id, curve: new id, x: number, [offset: (x, y)])constructor / setupa live readout of a curve’s slope at x
slopetri(id: new id, curve: new id, x: number, [run: number])constructor / setupa 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 / setupthe filled region under a curve from a to b
integral(id: new id, curve: new id, a: number, b: number, [at: (x, y)])constructor / setupa live readout of the integral of a curve from a to b
roots(id: new id, curve: new id, [color: colour])constructor / setupdots where a curve crosses zero
deriv(id: new id, curve: new id, [color: colour])constructor / setupthe derivative f’ of a curve, drawn as its own curve
accum(id: new id, curve: new id, [a: number], [color: colour])constructor / setupthe accumulation function integral(a..x) of a curve
extrema(id: new id, curve: new id, [color: colour])constructor / setupdots at a curve’s maxima and minima (slope = 0)
inflections(id: new id, curve: new id, [color: colour])constructor / setupdots where a curve changes concavity (f’’ = 0)
band(id: new id, top: new id, bottom: new id, [color: colour], [range: (x, y)])constructor / setupthe 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 / setupa 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 / setupa 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 / setupa 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 / setupa 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 / setupRiemann 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 / setupthe degree-n Taylor polynomial of a curve about a
limit(id: new id, curve: new id, a: number, [color: colour])constructor / setupvisualise lim(x->a) f(x): the value approached, with an approaching dot
newton(id: new id, curve: new id, x0: number, [steps: number])constructor / setupNewton’s method zig-zag from a starting guess
spline(…)constructor / setupa 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 / setupan 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 / setupa 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 / setupan 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 / setupthe 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 / setupa 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 / setupA = 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 / setupA = 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 / setupanimated 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 / setuporthogonal projection of a vector onto a line (subspace)
leastsquares(id: new id, center: (x, y), unit: number, data: string, [color: colour])constructor / setupthe 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 / setupthe 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 / setupthe 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 / setupan arrow from an origin
numberline(…)constructor / setupa labelled number line
numberplane(…)constructor / setupa gridded coordinate plane
plane(…)constructor / setupa coordinate plane
complexplane(…)constructor / setupthe complex plane
polarplane(…)constructor / setupa polar grid
matrix(id: new id, data: string, center: (x, y), [colw: number], [rowh: number])constructor / setupa bracketed matrix
table(id: new id, data: string, center: (x, y), [colw: number], [rowh: number], [cols: string], [rows: string])constructor / setupa ruled table
mathtable(…)constructor / setupa table of math expressions
integertable(…)constructor / setupa table of integers
decimaltable(…)constructor / setupa table of decimals
arc(…)constructor / setupa circular arc
sector(…)constructor / setupa pie sector
annulus(…)constructor / setupa ring / annular sector
pie(…)constructor / setupa pie chart
arrowfield(…)constructor / setupa named or formula-authored bounded vector field
vectorfield(…)constructor / setupa named or formula-authored bounded vector field
tangent(…)constructor / setuptangent line to a curve at x (or tangent points to a circle)

Process

APIKindWhat it does
livehistogram(id: new id, center: (x, y), min: number, max: number, bins: number, [width: number], [height: number], [color: colour])constructor / setupan 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 verbprogressively 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 verbrelease 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 verbcarry 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 verbsend 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 verbcatch 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 verbdrive a counter or live histogram from measurements produced by the latest stream, emit, or branch process

Charts

APIKindWhat it does
racechart(id: new id, layout: string, periods: string, [title: string])constructor / setupdeclare 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 / setuppaste 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 / setupadd 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 / setupadd 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 / setupadd 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 verbplay 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

APIKindWhat it does
architecture(id: new id, [center: (x, y)], [width: number], [height: number])constructor / setupan 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 / setupan 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 / setupa 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 / setupa 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 / setupa 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 / setupa dashed directed possibility with an optional bend or port-aware orthogonal route
annotate(edge: id or tag, text: string)constructor / setupa 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 / setupa persistent generic message starting at one system node
request(id: new id, source: id or tag, label: string)constructor / setupa persistent labelled request starting at one system node
route(message: id or tag, connection: id or tag, [duration: number], [ease: easing])state-changing verbmove one persistent message through a continuous named connection and illuminate its selected lane
hotpath(message: id or tag, [duration: number], [seed: number])state-changing verbmove one persistent message end-to-end over a seeded valid path, choosing one lane at each fan-out

Geometry

APIKindWhat it does
point(id: new id, at: (x, y), [label: string])constructor / setupa labelled point
segment(id: new id, a: id or tag, b: id or tag)constructor / setupsegment between two points
midpoint(id: new id, a: id or tag, b: id or tag)constructor / setupmidpoint of two points
centroid(…)constructor / setupcentroid of a triangle
circumcenter(…)constructor / setupcircumcentre
incenter(…)constructor / setupincentre
orthocenter(…)constructor / setuporthocentre
foot(…)constructor / setupfoot of a perpendicular
meet(…)constructor / setupline-line intersection
linecircle(…)constructor / setupline-circle intersection
circlecircle(…)constructor / setupcircle-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 / setupa 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 / setupreflect a point over a line
bisector(…)constructor / setupangle bisector
rotpoint(…)constructor / setuprotate a point about another
between(…)constructor / setupa point between two others
anglepoint(…)constructor / setupa point at an angle
circumcircle(…)constructor / setupcircumscribed circle
incircle(…)constructor / setupinscribed circle
circle2(…)constructor / setupcircle from centre + a point on it
ellipse(…)constructor / setupan ellipse
parabola(…)constructor / setupa parabola
hyperbola(…)constructor / setupa hyperbola
fullline(…)constructor / setupan infinite line through two points
anglemark(…)constructor / setupan angle arc mark (optional label rides the bisector)
rightangle(…)constructor / setupa right-angle square mark

Algorithms and data structures

APIKindWhat it does
graph(id: new id, verts: string, edges: string, layout: new id, center: (x, y), scale: number, [radius: number])constructor / setupa node/edge graph (weights via a-b:w)
array(id: new id, vals: string, center: (x, y), [cw: number], [ch: number])constructor / setupa row of value cells in slot boxes
list(id: new id, vals: string, center: (x, y), [kind: new id], [cw: number], [ch: number])constructor / setupa linked list (singly/doubly/circular)
stack(id: new id, center: (x, y), [cw: number], [ch: number])constructor / setupa stack (LIFO, grows up)
queue(id: new id, center: (x, y), [cw: number], [ch: number])constructor / setupa queue (FIFO, grows right)
hashmap(id: new id, n: number, center: (x, y), [ew: number], [ch: number])constructor / setupn buckets with separate chaining
pointer(id: new id, arr: id or tag, slot: number, [label: string])constructor / setupan index caret under an array slot
caret(id: new id, at: (x, y), label: string, [dir: new id])constructor / setupa labelled triangle marker
compare(arr: id or tag, i: number, j: number, [color: colour])animation verbflash the values in two array slots
pointat(id: id or tag, arr: id or tag, slot: number, [dur: number])animation verbslide an index pointer to a slot
push(id: id or tag, value: string, [dur: number])state-changing verbpush onto a stack
pop(id: id or tag, [dur: number])state-changing verbpop the top of a stack
enqueue(id: id or tag, value: string, [dur: number])state-changing verbenqueue at the back
dequeue(id: id or tag, [dur: number])state-changing verbdequeue from the front
insert(id: id or tag, after: number, value: string, [dur: number])state-changing verbsplice a node into a list
remove(id: id or tag, index: number, [dur: number])state-changing verbunlink a list node
put(id: id or tag, key: string, val: string, [dur: number])state-changing verbhash a key into a bucket + chain
get(id: id or tag, key: string, [dur: number])animation verbscan a bucket’s chain for a key
bfs(g: id or tag, start: id or tag)state-changing verbbreadth-first traversal (queue)
dfs(g: id or tag, start: id or tag)state-changing verbdepth-first traversal (stack)
dijkstra(g: id or tag, start: id or tag)state-changing verbsingle-source shortest paths

Circuit

APIKindWhat it does
circuit(id: new id, center: (x, y), netlist: string, [unit: number], [labels: number], [build: number])constructor / setupdraw 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 / setupa 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 / setuphow 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 verbTAKE 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 verbput 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 / setupthe 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

APIKindWhat it does
grid(id: new id, [center: (x, y)], [cols: number], [rows: number], [cellsize: number], [seeded_cellsize: number])constructor / setupa 2D cell grid (arg1 a “# . @ *” spec seeds it, else it is the centre)
neighbors(id: id or tag, mode: string)constructor / setupgrid connectivity: “4” (default) or “8” (diagonals)
setcell(id: id or tag, r: number, c: number, kind: id or tag)constructor / setupset one cell’s kind (wall/open/start/goal)
walls(id: id or tag, cells: string)constructor / setupbatch-set several cells to wall: “r,c r,c …”
gridbfs(id: id or tag, start: (x, y), goal: (x, y))state-changing verbunweighted 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 verbA* pathfinding with a heuristic (manhattan/euclidean/diagonal)
evolve(id: id or tag, rule: string)constructor / setuppre-simulate one CA generation (“life” or a Golly rulestring)
collapse(id: id or tag, tileset: string, [seed: number])constructor / setuppre-simulate a seeded Wave-Function-Collapse settling

Chem

APIKindWhat it does
structure(id: new id, source: string, [center: (x, y)], [unit: number], [spec: string])constructor / setupa 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 / setupa 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 / setupsolve 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 / setupthe 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 verbwrite 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 / setupa 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 verbplay 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 verbmove 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 / setupthe 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 / setupthe 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 verban 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 / setupa 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 verblet 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 / setupan 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 verbthe 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 / setupa 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 / setupthe 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 verbturn 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 / setupa 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 verbanimate 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 / setupthe 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 / setupwhat 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 / setupthe 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 verbrun 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

APIKindWhat it does
banner(id: new id, center: (x, y), [scale: number])constructor / setupthe manic logo/banner
watermark(id: new id, [at: (x, y)], [text: string])constructor / setupa screen-fixed watermark; omit the point for responsive bottom-right placement or pass an exact point

Stats

APIKindWhat it does
histogram(id: new id, center: (x, y), data: string, [bins: number], [width: number], [height: number], [color: colour])constructor / setupbin a dataset into bars — the shape of the data
summary(id: new id, center: (x, y), data: string, [width: number], [color: colour])constructor / setupdescribe a dataset: mean/median/mode markers, spread, and readouts
correlation(id: new id, center: (x, y), unit: number, data: string, [color: colour])constructor / setupscatter + 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 / setupa histogram labelled with its skewness (left / right / symmetric)
boxplot(id: new id, center: (x, y), data: string, [width: number], [color: colour])constructor / setupfive-number summary as a box-and-whisker (box = IQR)
hypothesis(id: new id, center: (x, y), z: number, [alpha: number], [unit: number])constructor / setupa two-tailed significance test: p-value as shaded normal tails
covariance(id: new id, center: (x, y), unit: number, data: string, [color: colour])constructor / setupcovariance as signed-area rectangles about the mean cross
bayes(id: new id, center: (x, y), heads: number, tails: number, [width: number], [height: number])constructor / setupBayesian 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 / setupa 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 / setupa 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 / setupestimate 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 / setupa 2D random walk path from the centre
bellcurve(id: new id, center: (x, y), mu: number, sigma: number, [unit: number], [color: colour])constructor / setupthe 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 / setupthe 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 / setupLaw 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 / setupCentral Limit Theorem: sample means of dice pile into a bell

Physics

APIKindWhat it does
freekick(id: new id, center: (x, y), speed: number, angle: number, spin: number, [scale: number], [steps: number])constructor / setupan 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 / setupa 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 / setupa mass on a spring (pre-simulated); animate with run(id)
doublependulum(id: new id, [center: (x, y)], [angle1: number], [angle2: number], [unit: number])constructor / setupthe 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 / setupan elastic pendulum (swings + bounces); animate with run(id)
kapitza(id: new id, [center: (x, y)], [angle0: number], [vibeamp: number], [unit: number])constructor / setupa Kapitza pendulum — fast vibration stabilises the inverted position
cartpendulum(id: new id, [center: (x, y)], [angle0: number], [unit: number])constructor / setupa pendulum on a spring-mounted cart (control-theory system)
comparependulum(id: new id, [center: (x, y)], [angle0: number], [unit: number])constructor / setuptwo chaotic pendulums a hair apart — sensitive dependence
verticalspring(id: new id, [center: (x, y)], [stretch0: number], [unit: number], [damping: number])constructor / setupa mass bobbing on a vertical spring under gravity
springincline(id: new id, [center: (x, y)], [angle: number], [unit: number], [damping: number])constructor / setupa mass on a spring on an inclined plane
bungee(id: new id, [center: (x, y)], [unit: number], [damping: number])constructor / setupa bungee jump — free-fall then a one-sided elastic cord
resonance(id: new id, [center: (x, y)], [drivefreq: number], [unit: number], [damping: number])constructor / setupa driven spring — amplitude peaks near the natural frequency
doublespring(id: new id, [center: (x, y)], [unit: number])constructor / setuptwo masses coupled by springs — beating / normal modes
seriesparallel(id: new id, [center: (x, y)], [unit: number])constructor / setupsprings in series vs parallel — soft/slow vs stiff/fast
carsuspension(id: new id, [center: (x, y)], [unit: number])constructor / setupa quarter-car riding a scrolling road (bump/washboard/pothole)
piston(id: new id, [center: (x, y)], [rpm: number], [unit: number])constructor / setupan engine piston — a spinning crank drives a slider in a cylinder
molecule(id: new id, [center: (x, y)], [atoms: number], [unit: number])constructor / setupN atoms bonded by springs, vibrating about their shape
robotarm(id: new id, [center: (x, y)], [mode: number], [unit: number])constructor / setupa 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 / setupa vertical Atwood machine — two masses over one pulley
pulleyscale(id: new id, [center: (x, y)], [m1: number], [m2: number], [unit: number])constructor / setupan 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 / setupa 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 / setupa 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 / setupa block sliding on an inclined plane with friction
dropmass(id: new id, [center: (x, y)], [dropheight: number], [unit: number])constructor / setupa 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 / setupa 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 / setuptwo 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 / setupa 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 / setupthree 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 / setupa 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 / setuptwo 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 / setupa bullet fired into a block embeds (perfectly inelastic — most kinetic energy lost)
newtonscradle(id: new id, [center: (x, y)], [balls: number], [pulled: number])constructor / setupNewton’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 / setupN 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 / setuplabel 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 / setupa 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 / setupa 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 / setupa 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 / setupdominos 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 / setupa 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 / setupa person walking on a floating raft — centre of mass stays fixed
brachistochrone(id: new id, [center: (x, y)], [unit: number])constructor / setupfour beads race down different curves under gravity — the cycloid wins
run(id: id or tag, [dur_or_gens: number], [dur: number])animation verbreplay a sim’s motion over [dur] s (or a grid’s frames: run(grid, [gens], [dur]))
swing(id: id or tag, [dur: number])animation verbreplay a pendulum’s motion over [dur] seconds (alias of run)
forces(id: id or tag, [dur: number])animation verbreveal a sim’s free-body force diagram (ramp: gravity, normal, friction, a)
phase(id: id or tag, center: (x, y), [size: number])constructor / setupa sim’s phase portrait (e.g. θ vs ω); animates with swing
well(id: id or tag, center: (x, y), [size: number])constructor / setupa sim’s potential-energy well with the body as a rolling ball
timegraph(id: id or tag, center: (x, y), [size: number])constructor / setupa sim’s phase variables as curves over time (with a sweep line)
energygraph(id: id or tag, center: (x, y), [size: number])constructor / setupKE/PE/total energy as curves over time (with a sweep line)

Rubik

APIKindWhat it does
cube(id: new id, n: number, center: (x, y), [size: number], [view: new id], [state: string], [scramble: number])constructor / setupa 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 / setupthe 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 verbapply 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

APIKindWhat it does
network(id: new id, center: (x, y), layers: string, activations: string, [width: number], [height: number], [seed: number])constructor / setupa 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 / setupa truthful plot of linear, ReLU, sigmoid, or tanh
forward(network: id or tag, input: string, [dur: number], [ease: easing])state-changing verbcompute 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 verbthe 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 verbcompare 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 verbcompute 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 verbapply one gradient-descent parameter update and recompute the prediction and loss
checkpoint(id: new id, network: id or tag)state-changing verbsave 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 verbrestore(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 / setupa 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 / setupa 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 / setupa 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 / setupcompute 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 / setupcompute deterministic max or average pooling independently across tensor channels
scan(output: id or tag, [dur: number], [ease: easing])state-changing verbcoordinate 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 / setupsplit 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 / setupturn 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 / setupcompute 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 verbreveal 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 / setupproject 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 verbfilter, 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 / setupcompute 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 verbfocus 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 / setupproject one attention residual into an authored candidate vocabulary and draw the exact top-k softmax probabilities

Optics

APIKindWhat it does
refract(id: new id, [center: (x, y)], [n1: number], [n2: number], [angle: number])constructor / setupa 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 / setupa 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 / setupwhite 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 / setupchromatic 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 / setupa 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 / setupthe 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 / setupthe 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 / setupthe 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

APIKindWhat it does
creator(id: new id, spec: string)constructor / setupa 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 / setupdraw 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 / setupstart 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 / setupadd one of up to six fitted answer cards with stable A–F semantic tags; a trailing correct marks the right one
timing(…)constructor / setupgeneric named-phase timing controller (optional position) or quiz-specific pace/phase configuration; use generic controllers with timed/during
timerstyle(…)constructor / setupstyle 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 / setupa 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 / setupa 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 / setupfit 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 / setupattach optional author-supplied answer context and source to a quiz reveal
endcard(profile: id or tag, [spec: string])constructor / setupbuild 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:9not 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.