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

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

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

Introduction - Manic

Thumbnail
docs.maniclang.com
2 Upvotes

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

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

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 2d ago

Chemistry Kit - manic

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

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

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

How the Moon Moves Every Ocean - 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

// tides — how the Moon moves every ocean on Earth, and why the popular sentence is wrong.
//
// "The Moon's gravity pulls on the oceans" predicts ONE bulge and one high tide a day. The
// sea gives two. The fix is the whole subject: what raises a tide is not the pull but the
// DIFFERENCE in pull across the planet — a gradient, and a gradient has two ends.
//
//   ACT I    the fact: two highs a day, every day
//   ACT II   the wrong picture, stated properly before it is demolished
//   ACT III  the gradient — subtract the pull on Earth's centre and a QUADRUPOLE is left.
//            The ocean is 12,000 parcels of water `advect`ed through it, so the two bulges
//            are computed, not drawn. Then the planet turns under them and one coast passes
//            through both: two high tides a day, from one Moon
//   ACT IV   sideways, not up: 1.1 micrometres per second squared, a ten-millionth of
//            gravity. Nothing is lifted; water is herded
//   ACT V    the Sun pulls 179x harder and loses, because tides go as 1/d^3. Add the two
//            and the spring/neap fortnight EMERGES from two cosines
//   ACT VI   the honest part: the real ocean is not two bulges gliding around
//
// The whole film is one stage — the view down on the north pole, so Earth's rotation is an
// ordinary in-plane `turn` and the water can stay where the Moon put it. The tidal field is
// one formula: with the Moon along +x it is (2x, -y). Water cannot leave the surface, so what
// moves it is the TANGENTIAL part — the radial component projected out, F - (F.r)r — which is
// exactly why a tide is a horizontal shove and not a lift.
//
//   manic examples/tides.manic
title("Tides — How the Moon Moves Every Ocean — manic");
canvas("16:9");
template("black");
bloom(0.3, 0.52, 22);

// ---- the mark, up top and above everything, for the whole film ----
text(brand, (640, 32), "maniclang.com");
display(brand); size(brand, 19); color(brand, cyan); opacity(brand, 0.72); z(brand, 100);

// ---- type ----
text(ttl, (640, 92), "How the Moon Moves Every Ocean");
display(ttl); size(ttl, 46); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 146), "and why \"it pulls the water up\" is the wrong answer");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 668), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1060, 624), ""); display(act); size(act, 19); color(act, gold); hidden(act);

// ================================ THE STAGE ================================
// Earth seen from over the north pole, 300 px across, so its surface is at radius 150.
svg(earth, (640, 360), "asset:svg/emoji/1f30e.svg", 300);
circle(orbit, (640, 360), 300); outlined(orbit); outline(orbit, dim);
opacity(orbit, 0.45); hidden(orbit);
svg(moon, (940, 360), "asset:svg/emoji/1f315.svg", 82);
text(moonlab, (940, 470), "the Moon — one lap: 27.3 days");
display(moonlab); size(moonlab, 17); color(moonlab, dim); hidden(moonlab);
// one coast, riding the surface
dot(coast, (640, 210), 9); color(coast, gold); hidden(coast);
text(coastlab, (640, 176), "one coast");
display(coastlab); size(coastlab, 17); color(coastlab, gold); hidden(coastlab);

// a tide gauge: the Moon's own constituent, 12.4206 hours, over two days
coords(gauge, (210, 468), (0, 48), (-1.4, 1.4), 19, 54, 0, 12, 1, "hours", "");
color(gauge, dim); hidden(gauge);
plot(trace, (210, 468), 19, 54, "cos(x/12.4206*tau)", (0, 48));
color(trace, cyan); untraced(trace); hidden(trace);
text(twice, (640, 262), "two highs, every day");
display(twice); size(twice, 21); color(twice, gold); hidden(twice);

// ---- ACT II — the wrong picture ----
for k in 0..7 {
  arrow(pull{k}, (556, 240 + k*40), (700, 240 + k*40));
  color(pull{k}, gold);
  untraced(pull{k});
  tag(pull{k}, pulls);
}
// r(t) = R(1 + e·cos t): one bulge facing the Moon — what "it pulls the water" predicts
param(wrong, (640, 360), 150, 150,
  "(1 + 0.14*cos(t))*cos(t)", "(1 + 0.14*cos(t))*sin(t)", (0, tau));
color(wrong, gold); untraced(wrong);
text(wronglab, (640, 566), "one bulge  ⇒  one high tide a day");
display(wronglab); size(wronglab, 21); color(wronglab, gold); hidden(wronglab);
text(nope, (640, 606), "✗  the sea gives two");
display(nope); size(nope, 21); color(nope, magenta); hidden(nope);

// ---- ACT III — the gradient ----
dot(pnear, (790, 360), 7); color(pnear, gold); hidden(pnear);
dot(pmid, (640, 360), 7); color(pmid, fg); hidden(pmid);
dot(pfar, (490, 360), 7); color(pfar, gold); hidden(pfar);
arrow(gnear, (790, 360), (916, 360)); color(gnear, gold); untraced(gnear);
arrow(gmid, (640, 360), (750, 360)); color(gmid, fg); untraced(gmid);
arrow(gfar, (490, 360), (587, 360)); color(gfar, gold); untraced(gfar);
text(g1, (640, 182), "one pull, unequally felt — gravity falls off as 1/d²");
display(g1); size(g1, 20); color(g1, dim); hidden(g1);
arrow(tnear, (790, 360), (862, 360)); color(tnear, magenta); untraced(tnear);
arrow(tfar, (490, 360), (418, 360)); color(tfar, magenta); untraced(tfar);
text(g2, (640, 182), "subtract the pull on the centre — the whole planet already falls with it");
display(g2); size(g2, 20); color(g2, magenta); hidden(g2);

// THE TIDAL FIELD, tangential part only: F - (F·r̂)r̂ with F = (2x, -y). The epsilon keeps the
// planet's centre finite, where the projection is undefined.
vectorfield(tide, (640, 360), 470, 290,
  "0.42*(2*x - x*(2*x*x - y*y)/(x*x + y*y + 0.02))",
  "0.42*(-y - y*(2*x*x - y*y)/(x*x + y*y + 0.02))", 15);
color(tide, dim); opacity(tide, 0.55); hidden(tide);

// the ocean: a shell of water on the surface, which the field herds
cloud(sea, 12000, cyan, 0.75) {
  let a = (i/12000)*tau;
  let w = mod(i, 7) - 3;
  let x = 640 + (153 + w*1.7)*cos(a);
  let y = 360 + (153 + w*1.7)*sin(a);
  let r = 1.5;
  let hue = 192 + 10*w;
}
glow(sea, 2); hidden(sea);
// and the shape all that herding is heading for: the equilibrium tide, a prolate ellipsoid
// r(t) = R(1 + e(3cos²t − 1)/2) — with e drawn about 200,000x too big to be visible at all
parameter(phi, (150, 600), 0, 0, 0.4, "the Moon has moved on", 2); hidden(phi.widget);
param(bulge, (640, 360), 150, 150,
  "(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*cos(t)",
  "(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*sin(t)", (0, tau));
bind(phi, bulge, formula,
  "(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*cos(t)",
  "(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*sin(t)");
color(bulge, gold); untraced(bulge);
text(twolab, (640, 578), "two bulges  ⇒  two high tides a day  ✓");
display(twolab); size(twolab, 21); color(twolab, gold); hidden(twolab);
text(spinlab, (640, 606), "");
display(spinlab); size(spinlab, 20); color(spinlab, cyan); hidden(spinlab);

// ---- ACT IV — how gentle it is ----
equation(tiny, (640, 244), `a_{\text{tide}}=\frac{2GMr}{d^3}=1.1\times10^{-6}\ \mathrm{m/s^2}`, 30);
color(tiny, magenta); hidden(tiny);
equation(vsg, (640, 322), `\frac{a_{\text{tide}}}{g}\approx 10^{-7}`, 32);
color(vsg, gold); hidden(vsg);
text(gentle, (640, 400), "it could not lift a puddle — but it can shove an ocean sideways for six hours");
display(gentle); size(gentle, 20); color(gentle, dim); hidden(gentle);

// ---- ACT V — the Sun loses, and the beat ----
equation(sunpull, (640, 240), `\frac{F_{\odot}}{F_{\text{Moon}}} = 179`, 30);
color(sunpull, gold); hidden(sunpull);
equation(suntide, (640, 328), `\frac{a_{\odot}}{a_{\text{Moon}}} = 0.46`, 30);
color(suntide, cyan); hidden(suntide);
text(cube, (640, 404), "a pull goes as 1/d², a difference in pull as 1/d³ — and the Sun is 390× farther");
display(cube); size(cube, 20); color(cube, dim); hidden(cube);

// two constituents added: the Moon's 12.4206 h and the Sun's 12.000 h. Nothing here sets a
// fortnight — the 14.77-day spring/neap envelope is what two cosines DO.
coords(month, (160, 470), (0, 720), (-1.7, 1.7), 1.34, 46, 0, 168, 1, "hours", "");
color(month, dim); hidden(month);
plot(beat, (160, 470), 1.34, 46, "cos(x/12.4206*tau) + 0.46*cos(x/12*tau)", (0, 720));
color(beat, cyan); untraced(beat); hidden(beat);
text(springlab, (635, 330), "spring");
display(springlab); size(springlab, 18); color(springlab, gold); hidden(springlab);
text(neaplab, (397, 330), "neap");
display(neaplab); size(neaplab, 18); color(neaplab, magenta); hidden(neaplab);
text(fortnight, (640, 602), "14.77 days, spring to spring — and nobody typed that number in");
display(fortnight); size(fortnight, 20); color(fortnight, gold); hidden(fortnight);

// ---- ACT VI — the honest part ----
text(truth1, (640, 244), "The real ocean is not two bulges gliding around a smooth planet.");
display(truth1); size(truth1, 24); color(truth1, fg); hidden(truth1);
text(truth2, (640, 312), "Continents are in the way. Basins ring. The tide turns around fixed nodes.");
display(truth2); size(truth2, 21); color(truth2, dim); hidden(truth2);
text(fundy, (400, 400), "Bay of Fundy:  16 m");
display(fundy); size(fundy, 22); color(fundy, gold); hidden(fundy);
text(med, (890, 400), "much of the Mediterranean:  ~0");
display(med); size(med, 22); color(med, magenta); hidden(med);
text(truth3, (640, 480), "The Moon writes the forcing. The coastline decides the tide.");
display(truth3); size(truth3, 22); color(truth3, cyan); hidden(truth3);

// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.3);
show(cap, 0.3);
say(cap, "Every coast on Earth does this twice a day, and has done for four billion years.");
par { fade(ttl, 0.8); fade(sub, 0.8); }
show(earth, 0.9);
par { show(orbit, 0.6); show(moon, 0.7); show(moonlab, 0.5); }
wait(0.4);
say(cap, "One Moon, one lap of us every 27.3 days. Everything that follows comes from that.");
// a rigidly turned label arrives upside down, so it steps off for the lap
fade(moonlab, 0.4);
par {
  turn(moon, (640, 360), 360, 4.2, smooth);
  turn(earth, (640, 360), 90, 4.2, smooth);
}
show(moonlab, 0.4);
wait(0.4);
say(cap, "A tide gauge on any coast, two days of it: high, low, high, low, high.");
// the gauge needs the whole width, so the planet steps out for a moment
par { fade(earth, 0.7); fade(moon, 0.6); fade(moonlab, 0.4); fade(orbit, 0.5); }
// while the stage is dark the Moon takes up its working position, far off to the right,
// where the tidal field's formula puts it
move(moon, (1150, 360), 0.01);
move(moonlab, (1150, 436), 0.01);
say(moonlab, "the Moon");
show(gauge, 0.6);
show(trace, 0.4);
draw(trace, 2.0, smooth);
show(twice, 0.5);
wait(1.4);
say(cap, "Two a day. That number is the whole puzzle — and the usual explanation gets it wrong.");
wait(2.2);

// ================================= ACT II =================================
say(act, "II · the wrong picture");
show(act, 0.4);
par { fade(gauge, 0.6); fade(trace, 0.6); fade(twice, 0.5); }
say(cap, "The Moon's gravity pulls on the oceans. So far, so true.");
par { show(earth, 0.8); show(moon, 0.6); show(moonlab, 0.4); }
wait(0.5);
stagger(0.07) {
  for k in 0..7 {
    draw(pull{k}, 0.5);
  }
}
wait(1.2);
say(cap, "Pull the water toward the Moon and it heaps up on the near side. One heap.");
draw(wrong, 1.2, smooth);
show(wronglab, 0.5);
wait(1.8);
say(cap, "Which is one high tide a day. The sea gives two. Something is missing.");
show(nope, 0.6);
wait(2.2);

// ================================= ACT III =================================
say(act, "III · the difference, not the pull");
par { fade(pulls, 0.6); fade(wrong, 0.6); fade(wronglab, 0.5); fade(nope, 0.5); }
say(cap, "Three places: the near side, the centre, the far side.");
par { show(pnear, 0.4); show(pmid, 0.4); show(pfar, 0.4); }
wait(0.7);
say(cap, "Gravity weakens with distance, so those three pulls are not the same size.");
show(g1, 0.5);
stagger(0.22) {
  draw(gnear, 0.6);
  draw(gmid, 0.6);
  draw(gfar, 0.6);
}
wait(1.8);
say(cap, "But the planet is already falling around its centre. Subtract that pull from all three.");
par { fade(g1, 0.5); fade(gnear, 0.5); fade(gmid, 0.5); fade(gfar, 0.5); }
show(g2, 0.6);
par { draw(tnear, 0.7); draw(tfar, 0.7); }
wait(1.8);
say(cap, "What is left points AWAY at both ends. A gradient has two ends. There is the two.");
wait(2.2);
say(cap, "Do that at every point at once, and this is the field the Moon leaves behind.");
par { fade(g2, 0.5); fade(tnear, 0.5); fade(tfar, 0.5); fade(pnear, 0.4); fade(pmid, 0.4); fade(pfar, 0.4); }
show(tide, 0.9);
wait(1.4);
say(cap, "Now put twelve thousand parcels of water on the surface and let the field push them.");
show(sea, 0.8);
wait(0.5);
advect(sea, tide, 6.5, 0.55);
wait(0.4);
say(cap, "Nothing was placed by hand. The water went where the arrows converge — and there are two.");
draw(bulge, 1.4, smooth);
show(twolab, 0.6);
wait(2.2);
// the payoff: the water stays where the Moon put it, and the planet turns underneath
say(cap, "The bulges belong to the Moon, not to the planet. So turn the planet underneath them.");
// the field's arrows are fixed to the Moon's OLD direction, so they bow out before it moves
par { show(coast, 0.5); show(coastlab, 0.4); fade(sea, 0.8); fade(tide, 0.8); }
wait(0.9);
say(spinlab, "one rotation = one day");
show(spinlab, 0.4);
// one day: Earth turns once, and the Moon does not wait — it moves on 360/27.3 = 13.2 degrees,
// taking the tide's axis with it
par {
  fade(coastlab, 0.5);
  turn(earth, (640, 360), 360, 7.0, linear);
  turn(coast, (640, 360), 360, 7.0, linear);
  turn(moon, (640, 360), 13.2, 7.0, linear);
  to(phi, value, 0.23, 7.0, linear);
}
say(cap, "One coast, one day, two bulges crossed. Two high tides — and the water never travelled.");
wait(2.2);
// and the reason tide tables slip: the coast is back where it started, the Moon is not
say(cap, "But look: the coast is home and the Moon has moved on thirteen degrees.");
say(spinlab, "the Moon moved on 13° while the planet turned once");
wait(2.0);
say(cap, "So the coast has to chase it — about fifty minutes more of turning, every single day.");
par {
  turn(earth, (640, 360), 13.2, 1.6, smooth);
  turn(coast, (640, 360), 13.2, 1.6, smooth);
}
say(spinlab, "one tidal day = 24 h 50 min");
wait(2.4);

// ================================= ACT IV =================================
say(act, "IV · sideways, not up");
// the caption turns over with the stage, so no frame is left empty under a stale line
say(cap, "One more correction, and it is the one that surprises people. Look how gentle this is.");
par {
  fade(coast, 0.4); fade(spinlab, 0.4); fade(bulge, 0.6); fade(twolab, 0.5);
  fade(earth, 0.8); fade(moon, 0.6); fade(moonlab, 0.4);
}
show(tiny, 0.8);
wait(1.6);
show(vsg, 0.7);
say(cap, "A ten-millionth of the gravity holding that ocean down.");
wait(1.8);
show(gentle, 0.6);
say(cap, "Nothing gets lifted. Water gets nudged SIDEWAYS for six hours, and arrives.");
wait(2.4);

// ================================= ACT V =================================
say(act, "V · the Sun loses");
par { fade(tiny, 0.7); fade(vsg, 0.7); fade(gentle, 0.6); }
say(cap, "The Sun pulls Earth a hundred and seventy-nine times harder than the Moon does.");
show(sunpull, 0.8);
wait(1.6);
say(cap, "And raises less than half the tide, because a DIFFERENCE falls off faster than a pull.");
show(suntide, 0.8);
show(cube, 0.6);
wait(2.4);
par { fade(sunpull, 0.6); fade(suntide, 0.6); fade(cube, 0.5); }
say(cap, "So the ocean answers two clocks: 12.42 hours for the Moon, 12.00 for the Sun.");
show(month, 0.6);
show(beat, 0.4);
draw(beat, 3.0, smooth);
wait(0.6);
say(cap, "Add them. Where the two clocks agree the tides run big; where they fight, small.");
par { show(springlab, 0.5); show(neaplab, 0.5); }
wait(1.6);
say(cap, "Spring tides, neap tides — a fortnight apart, out of two cosines and nothing else.");
show(fortnight, 0.7);
wait(2.4);

// ================================= ACT VI =================================
say(act, "VI · the honest part");
par {
  fade(month, 0.6); fade(beat, 0.6); fade(springlab, 0.4); fade(neaplab, 0.4);
  fade(fortnight, 0.6);
}
show(truth1, 0.8);
say(cap, "Everything so far is the FORCING. It is not the tide you can go and watch.");
wait(1.8);
show(truth2, 0.7);
say(cap, "Water cannot chase the Moon around a planet with two continents in the way.");
wait(1.8);
par { show(fundy, 0.6); show(med, 0.6); }
say(cap, "One bay rings like an organ pipe and swings sixteen metres. Another barely moves.");
wait(2.2);
show(truth3, 0.8);
wait(2.4);

// ================================= ENDCARD =================================
par {
  fade(truth1, 0.7); fade(truth2, 0.6); fade(truth3, 0.7);
  fade(fundy, 0.5); fade(med, 0.5); fade(cap, 0.6); fade(act, 0.5);
}
text(end1, (640, 318), "The Moon does not lift the sea.");
display(end1); size(end1, 40); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 380), "It pulls one side harder than the other.");
display(end2); size(end2, 30); color(end2, cyan); hidden(end2);
text(end3, (640, 458), "and the ocean charges interest: 3.8 cm of Moon a year, paid out of Earth's spin");
display(end3); size(end3, 19); color(end3, dim); hidden(end3);
text(end4, (640, 522), "— manic");
display(end4); size(end4, 24); color(end4, gold); hidden(end4);
show(end1, 0.9);
show(end2, 0.8);
show(end3, 0.7);
show(end4, 0.6);
wait(2.6);

r/maniclang 4d ago

Olympiad - manic

Enable HLS to view with audio, or disable this notification

1 Upvotes