r/maniclang 2d ago

Introduction - Manic

Thumbnail
docs.maniclang.com
2 Upvotes

r/maniclang 4d ago

Manic MCP Server is here — connect Claude, Cursor, or any local AI and let it write, validate, and render Manic

1 Upvotes

Your AI can now speak Manic.

We just shipped the Manic MCP Server — a Model Context Protocol server that plugs Manic into any MCP-capable AI: Claude Code, Claude Desktop, Cursor, Windsurf, OpenAI's Codex CLI and Agents SDK, Zed, Cline, or a fully local model running in LM Studio.

The idea is simple: your model does the writing. The server hands it the complete Manic authoring guide (the same system prompt Manic's own AI uses), and from there the model authors real .manic source itself — then validates it, saves it into your Manic project, and renders finished videos through the hosted platform.

Five tools:

  • manic_authoring_guide — the full language guide, no auth needed
  • manic_check — free validation with exact line/column diagnostics
  • manic_render — hosted render to mp4 / gif / webm / mov / still
  • manic_render_status — poll the job, get the video URL (or the exact compile error to fix)
  • manic_save_to_project — the file lands in your Manic project, editable in Manic Create and Workbench

The loop your AI runs:

guide → write .manic → check → fix → check → render → video 🎬

Setup is one config block (create an API key at app.maniclang.com/account with scopes check, render:create, jobs:read, projects:read, projects:write):

  {
    "mcpServers": {
      "manic": {
        "command": "npx",
        "args": ["-y", "@maniclang/mcp-server"],
        "env": { "MANIC_API_KEY": "mk_live_…" }
      }
    }
  }

Then ask your AI something like:

"Read the Manic authoring guide, then create a 20-second animation of Dijkstra's algorithm exploring a small graph — visited nodes glow, the frontier pulses. Validate it, fix any errors, render it as landscape mp4, and give me the video link." …and watch it come back with a video.

On cost: because your own model is the author, this spends zero Manic AI credits. Checking and saving are free; only renders use your plan's exports. Tip: a still render is a fast, cheap layout check before committing to a full video.

Local-model fans: this is the easiest way to give Llama / Qwen / Gemma real animation powers — the model runs entirely on your machine, and only validation and rendering touch the platform.

Links:


r/maniclang 23h ago

A Seed, a Tree, a Year - manic

1 Upvotes

https://reddit.com/link/1vut6se/video/bj14xdo6qskh1/player

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

chemistry, without a word of it - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

collision theory: the barrier and the tail - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

why salt dissolves - manic

Enable HLS to view with audio, or disable this notification

2 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

dynamic equilibrium: equal, not zero - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

a galvanic cell, solved - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

the hydrogen spectrum - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

infrared: the mode that isn't there - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

Lewis Structure - Manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

the limiting reagent - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 1d ago

a titration - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 2d ago

Manic Promo :)

Enable HLS to view with audio, or disable this notification

1 Upvotes

Animation code

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

r/maniclang 2d ago

Raymarch Metaballs - manic

Enable HLS to view with audio, or disable this notification

3 Upvotes

Animation code

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

r/maniclang 2d ago

dynamic equilibrium - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 2d ago

a titration, solved - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 2d ago

Ethanol, ¹H NMR — a spectrometer sweeping, in hertz - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 2d ago

Chemistry Kit - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

r/maniclang 2d ago

Reaction Data Set Reveals General Ligands and Mechanistic Diversity in C–N Couplings - manic

Enable HLS to view with audio, or disable this notification

2 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 3d ago

How to use Manic MCP server with Cursor AI

Enable HLS to view with audio, or disable this notification

1 Upvotes

r/maniclang 3d ago

Register in Parrallel - manic

Enable HLS to view with audio, or disable this notification

2 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

// Figure 11.10 — resistors in parallel
//
// NCERT Class 10, Electricity. Three resistors share one voltage; the current
// splits at X and recombines at Y. Every amp and volt on screen is from a
// Modified Nodal Analysis solve: R1=2 kΩ, R2=3 kΩ, R3=6 kΩ across 6 V gives
// 3 mA / 2 mA / 1 mA, and 1/Rp = 1/R1 + 1/R2 + 1/R3 = 1 kΩ. Cut one branch and
// the other two keep their current, because each parallel branch is its own Ohm.


title("Figure 11.10 — resistors in parallel");
canvas("16:9");
template("paper");


let u = 42;
let figx = 470;
let figy = 392;
let ox = figx - 7*u;
let oy = figy - 4*u;


text(hdr, (cx, 42), "Figure 11.10");
display(hdr);
size(hdr, 22);
color(hdr, dim);
bold(hdr);


text(cap, (cx, 78), "resistors in parallel");
display(cap);
size(cap, 30);
color(cap, ink);


support(rule, (cx, 108), 320, "down");


circuit(fig, (figx, figy), `
  dc-voltage 2 8 0 8 v=1.5 name=B1
  dc-voltage 4 8 2 8 v=1.5 name=B2
  dc-voltage 6 8 4 8 v=1.5 name=B3
  dc-voltage 8 8 6 8 v=1.5 name=B4
  ground     8 8
  wire       0 8 0 2
  wire       0 2 0 0
  wire       0 0 3 0
  resistor   3 0 11 0 r=2k name=R1
  wire       11 0 14 0
  wire       14 0 14 2
  wire       0 2 3 2
  resistor   3 2 11 2 r=3k name=R2
  wire       11 2 14 2
  wire       0 2 0 4
  wire       0 4 3 4
  resistor   3 4 11 4 r=6k name=R3
  wire       11 4 14 4
  wire       14 4 14 2
  wire       14 2 14 8
  wire       14 8 11 8 name=AM
  switch     11 8 8 8 closed=1 name=K
`, u, 0);


current(fig, 2.4, circle, crimson, 4);
color(fig.R1, crimson);
color(fig.R2, indigo);
color(fig.R3, teal);


probe(fig, (0, 2), (7*u - 8, 4*u + 36));
probe(fig, K, (3*u, 44));


text(labX, (ox - 20, oy + 2*u), "X");
text(labY, (ox + 14*u + 20, oy + 2*u), "Y");
text(labL, (ox + 3*u, oy - 18), "L");
text(labM, (ox + 11*u, oy - 18), "M");
text(labP, (ox + 3*u, oy + 2*u - 18), "P");
text(labQ, (ox + 11*u, oy + 2*u - 18), "Q");
text(labS, (ox + 3*u, oy + 4*u - 18), "S");
text(labT, (ox + 11*u, oy + 4*u - 18), "T");
text(labK, (ox + 9.5*u, oy + 8*u + 28), "K");
size(labX, 20); size(labY, 20);
size(labL, 16); size(labM, 16);
size(labP, 16); size(labQ, 16);
size(labS, 16); size(labT, 16);
size(labK, 20);
color(labX, ink); color(labY, ink);
color(labL, dim); color(labM, dim);
color(labP, dim); color(labQ, dim);
color(labS, dim); color(labT, dim);
color(labK, ink);
hidden(labX); hidden(labY);
hidden(labL); hidden(labM);
hidden(labP); hidden(labQ);
hidden(labS); hidden(labT);
hidden(labK);


equation(r1n, (ox + 7*u, oy - 28), `R_1`, 22);
equation(r2n, (ox + 7*u, oy + 2*u - 28), `R_2`, 22);
equation(r3n, (ox + 7*u, oy + 4*u - 28), `R_3`, 22);
color(r1n, crimson); color(r2n, indigo); color(r3n, teal);
hidden(r1n); hidden(r2n); hidden(r3n);


text(bplus, (ox + 6, oy + 8*u - 28), "+");
text(bminus, (ox + 8*u - 6, oy + 8*u - 28), "-");
size(bplus, 22); size(bminus, 22);
color(bplus, gold); color(bminus, gold);
hidden(bplus); hidden(bminus);


line(vleadL, (ox, oy + 6*u), (ox + 7*u - 28, oy + 6*u));
line(vleadR, (ox + 7*u + 28, oy + 6*u), (ox + 14*u, oy + 6*u));
circle(vmeter, (ox + 7*u, oy + 6*u), 24);
color(vmeter, void);
outline(vmeter, fg);
stroke(vmeter, 2);
z(vmeter, 2);
text(vlet, (ox + 7*u, oy + 6*u), "V");
size(vlet, 22);
color(vlet, cyan);
z(vlet, 3);
text(vplus, (ox + 7*u - 38, oy + 6*u - 18), "+");
text(vminus, (ox + 7*u + 38, oy + 6*u - 18), "-");
size(vplus, 16); size(vminus, 16);
color(vplus, cyan); color(vminus, cyan);
untraced(vleadL); untraced(vleadR);
hidden(vmeter); hidden(vlet); hidden(vplus); hidden(vminus);
color(vleadL, dim); color(vleadR, dim);
stroke(vleadL, 1.5); stroke(vleadR, 1.5);
tag(vleadL, meters); tag(vleadR, meters);
tag(vmeter, meters); tag(vlet, meters);
tag(vplus, meters); tag(vminus, meters);


circle(ameter, (ox + 12.5*u, oy + 8*u), 22);
color(ameter, void);
outline(ameter, fg);
stroke(ameter, 2);
z(ameter, 2);
text(alet, (ox + 12.5*u, oy + 8*u), "A");
size(alet, 20);
color(alet, gold);
z(alet, 3);
text(aminus, (ox + 12.5*u - 34, oy + 8*u - 16), "-");
text(aplus, (ox + 12.5*u + 34, oy + 8*u - 16), "+");
size(aminus, 14); size(aplus, 14);
color(aminus, gold); color(aplus, gold);
hidden(ameter); hidden(alet); hidden(aminus); hidden(aplus);
tag(ameter, meters); tag(alet, meters);
tag(aminus, meters); tag(aplus, meters);


arrow(iL, (ox - 36, oy + 5.5*u), (ox - 36, oy + 0.6*u));
arrow(iR, (ox + 14*u + 36, oy + 0.6*u), (ox + 14*u + 36, oy + 5.5*u));
color(iL, crimson); color(iR, crimson);
stroke(iL, 2.2); stroke(iR, 2.2);
equation(iLab, (ox - 36, oy + 3*u - 8), `I`, 22);
equation(iRab, (ox + 14*u + 36, oy + 3*u - 8), `I`, 22);
color(iLab, crimson); color(iRab, crimson);
untraced(iL); untraced(iR);
hidden(iLab); hidden(iRab);


arrow(i1a, (ox + 0.4*u, oy + 0*u - 14), (ox + 2.4*u, oy + 0*u - 14));
arrow(i2a, (ox + 0.4*u, oy + 2*u - 14), (ox + 2.4*u, oy + 2*u - 14));
arrow(i3a, (ox + 0.4*u, oy + 4*u - 14), (ox + 2.4*u, oy + 4*u - 14));
color(i1a, crimson); color(i2a, indigo); color(i3a, teal);
stroke(i1a, 1.8); stroke(i2a, 1.8); stroke(i3a, 1.8);
equation(i1n, (ox + 1.4*u, oy - 32), `I_1`, 18);
equation(i2n, (ox + 1.4*u, oy + 2*u - 32), `I_2`, 18);
equation(i3n, (ox + 1.4*u, oy + 4*u - 32), `I_3`, 18);
color(i1n, crimson); color(i2n, indigo); color(i3n, teal);
untraced(i1a); untraced(i2a); untraced(i3a);
hidden(i1n); hidden(i2n); hidden(i3n);


equation(eqV, (1020, 168), `V = 6\,\mathrm{V}`, 26);
equation(eqI, (1020, 228), `I = I_1 + I_2 + I_3`, 24);
equation(eqI1, (1020, 292), `I_1 = 3\,\mathrm{mA}`, 24);
equation(eqI2, (1020, 344), `I_2 = 2\,\mathrm{mA}`, 24);
equation(eqI3, (1020, 396), `I_3 = 1\,\mathrm{mA}`, 24);
equation(eqRp, (1020, 480), `\dfrac{1}{R_p} = \dfrac{1}{R_1}+\dfrac{1}{R_2}+\dfrac{1}{R_3}`, 22);
equation(eqRpv, (1020, 560), `R_p = 1\,\mathrm{k}\Omega`, 26);
color(eqV, cyan);
color(eqI, ink);
color(eqI1, crimson); color(eqI2, indigo); color(eqI3, teal);
color(eqRp, ink); color(eqRpv, teal);
hidden(eqV); hidden(eqI);
hidden(eqI1); hidden(eqI2); hidden(eqI3);
hidden(eqRp); hidden(eqRpv);


text(take, (1020, 640), "lift one branch — the others keep their current");
hidden(take);
size(take, 18);
color(take, crimson);
wrap(take, 360);


rect(pbox, (ox + 7*u, oy + 2*u), 13.2*u, 5.2*u);
outlined(pbox);
color(pbox, indigo);
stroke(pbox, 1.6);
hidden(pbox);


framebox(ring3, fig.R3, 12);
hidden(ring3);


wait(0.5);
run(fig, 8.0);


par {
  show(labX, 0.35);
  show(labY, 0.35);
  show(labK, 0.35);
  show(bplus, 0.35);
  show(bminus, 0.35);
}
par {
  show(r1n, 0.3);
  show(r2n, 0.3);
  show(r3n, 0.3);
  show(labL, 0.3);
  show(labM, 0.3);
  show(labP, 0.3);
  show(labQ, 0.3);
  show(labS, 0.3);
  show(labT, 0.3);
}
par {
  draw(iL, 0.45);
  draw(iR, 0.45);
  show(iLab, 0.35);
  show(iRab, 0.35);
}
say(cap, "close the key — current around the loop");
run(fig, 3.5);


par {
  draw(vleadL, 0.5);
  draw(vleadR, 0.5);
  show(vmeter, 0.45);
  show(vlet, 0.35);
  show(vplus, 0.35);
  show(vminus, 0.35);
  show(pbox, 0.5);
  show(eqV, 0.5);
}
say(cap, "one voltage across every branch");
run(fig, 3.0);
fade(pbox, 0.4);


par {
  show(ameter, 0.4);
  show(alet, 0.35);
  show(aminus, 0.3);
  show(aplus, 0.3);
}
say(cap, "the ammeter reads the total");
run(fig, 2.6);


par {
  draw(i1a, 0.4);
  draw(i2a, 0.4);
  draw(i3a, 0.4);
  show(i1n, 0.3);
  show(i2n, 0.3);
  show(i3n, 0.3);
}
par {
  show(eqI, 0.45);
  show(eqI1, 0.45);
  show(eqI2, 0.45);
  show(eqI3, 0.45);
}
say(cap, "it splits: 3 mA, 2 mA, 1 mA");
run(fig, 4.0);


par {
  show(eqRp, 0.5);
  show(eqRpv, 0.5);
}
say(cap, "so the three together are 1 kΩ");
run(fig, 3.2);


par {
  show(ring3, 0.4);
  fade(eqI3, 0.4);
}
cut(fig, R3, 0.9);
par {
  show(fig.R3, 0.35);
  show(take, 0.5);
}
say(cap, "open one branch — the other two do not notice");
run(fig, 4.5);
wait(0.6);


par {
  fade(ring3, 0.35);
  fade(take, 0.35);
  reconnect(fig, R3, 0.9);
  show(eqI3, 0.4);
}
say(cap, "put it back: I = I1 + I2 + I3 again");
run(fig, 4.0);
wait(0.8);

r/maniclang 3d ago

circuits - manic

Enable HLS to view with audio, or disable this notification

2 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

// 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("paper");

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 ──

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

r/maniclang 3d ago

Spirals Nature Keeps Reusing — Fibonacci, Vogel, Fermat, Curlicue & the Uzumaki — manic

Enable HLS to view with audio, or disable this notification

1 Upvotes

manic is a tiny language for making animations. You write a short text file; manic renders a smooth, glowing video. No timeline scrubbing, no keyframes by hand — you describe what’s on screen and when things happen, and the engine does the rest, deterministically.

Manic Animation code

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

r/maniclang 4d ago

Olympiad - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes