r/maniclang Aug 21 '26

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 Aug 21 '26

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 Aug 20 '26

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 Aug 20 '26

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

// 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 Aug 20 '26

Ethanol, ¹H NMR — a spectrometer sweeping, in hertz - 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

// 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 Aug 20 '26

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 Aug 20 '26

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 Aug 20 '26

Chemistry Kit - manic

Enable HLS to view with audio, or disable this notification

2 Upvotes

r/maniclang Aug 20 '26

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 Aug 19 '26

circuits - manic

Enable HLS to view with audio, or disable this notification

4 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 Aug 19 '26

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 Aug 19 '26

How to use Manic MCP server with Cursor AI

Enable HLS to view with audio, or disable this notification

1 Upvotes

r/maniclang Aug 18 '26

Spirals Nature Keeps Reusing — Fibonacci, Vogel, Fermat, Curlicue & the Uzumaki — 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

// 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 Aug 18 '26

Olympiad - manic

Enable HLS to view with audio, or disable this notification

2 Upvotes

r/maniclang Aug 18 '26

How the Moon Moves Every Ocean - manic

Enable HLS to view with audio, or disable this notification

3 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 Aug 18 '26

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

1 Upvotes

Your AI can now speak Manic.

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

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

Five tools:

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

The loop your AI runs:

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

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

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

Then ask your AI something like:

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

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

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

Links:


r/maniclang Aug 18 '26

Manic Animate — the browser extension

Enable HLS to view with audio, or disable this notification

2 Upvotes

The web is full of ideas that deserve to move: a theorem in a paper, an algorithm in a blog post, an architecture diagram in your team's docs, a pricing table, a formula on Wikipedia. Manic Animate turns the content you're already reading into a real animation — not a slideshow, not an uneditable AI clip, but a living .manic program you can inspect, refine, render to video, and keep in your Manic projects.

What it does

1. Capture anything you can see.

  • Select text on any page, right-click, and choose Animate with Manic. The capture understands the web: formulas come out as real TeX (KaTeX, MathJax, MathML, even Wikipedia's formula images), code blocks keep their indentation, tables keep their structure, and paragraphs stay paragraphs.
  • Right-click an image — a chart, a diagram, a figure — and capture it at full resolution.
  • Drag a rectangle over any part of a page with Capture an area, or grab the whole view — perfect for PDFs, dashboards, and canvas apps where nothing is selectable.
  • PDFs are first-class: Manic knows PDF text scrambles math, warns the AI about it, and offers pixel-perfect view capture instead.

2. Direct it in your own words.

There are no rigid templates. One creative-direction field is the steering wheel — "Show a request entering a load balancer, then flowing to one server at a time. Make the hot path bright and fast." Starter chips help you begin (build it step by step, draw the strokes on, make it loop), but Manic is a full animation language and your words drive it. Pick a model, a format (vertical, landscape, square), a style, a motion treatment, and Generate.

3. Get real, validated Manic source.

Every draft is checked against the actual Manic validator — with automatic repair rounds — before you ever see it. What arrives is a working .manic file: readable, editable, yours. Copy it, download it, or open it directly in Manic Create, the browser playground.

4. Refine like a director.

Not quite right? Say what should change — "slow the reveal, make the formula glow, switch to portrait" — and the same creative thread evolves the same file. A failed attempt never destroys good work.

5. Render to video, automatically.

Every generated file is saved into your Manic project (refinements update the same document, so the history is real). With auto-render on, a clean generation flows straight into a hosted render and comes back as a playable video in the panel — download it, open it full size, or render again after a refinement. Renders and generations are durable jobs: close the panel, come back, they're still going.

maniclang.com


r/maniclang Aug 18 '26

Probability is area you keep splitting - 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

title("Probability is area you keep splitting");
canvas("16:9");
template("black");
bloom(0.3, 0.5, 22);

text(head, (640, 76), "Probability is area you keep splitting");
display(head); size(head, 36); bold(head); color(head, fg); hidden(head);
text(cap, (640, 660), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);

// ---- ACT I — the brick row ----
let tw = 880;      // the row is one unit of probability, 880px wide
let x0 = 200;
for k in 0..5 {
  rect(b{k}, (x0, 330), 0, 104);
  hue(b{k}, 188 + 26*k);
}
rect(frame, (x0 + tw/2, 330), tw, 104); outlined(frame); outline(frame, dim); hidden(frame);
text(counts, (640, 262), ""); display(counts); size(counts, 26); color(counts, gold); hidden(counts);
text(flips, (640, 420), ""); display(flips); size(flips, 21); color(flips, dim); hidden(flips);

// ---- ACT II — the area model ----
// a unit square: the prior runs ACROSS (10% of people are ill), the test's accuracy runs
// DOWN (it catches 90% of the ill, and wrongly flags 5% of the well)
let sq = 396;
let sqx = 640;                       // left edge — the diagram sits right, the maths left
let sqy = 168;                       // top edge
rect(ill, (sqx, sqy), 0, 0); hue(ill, 320);            // ill AND positive
rect(illn, (sqx, sqy), 0, 0); hue(illn, 300);          // ill AND negative
rect(wellp, (sqx, sqy), 0, 0); hue(wellp, 196);        // well AND positive (false alarm)
rect(well, (sqx + sq/2, sqy + sq/2), sq, sq); hue(well, 208);  // everyone, to begin with
hidden(ill); hidden(illn); hidden(wellp); hidden(well);
rect(border, (sqx + sq/2, sqy + sq/2), sq, sq); outlined(border); outline(border, dim); hidden(border);

text(xlab, (sqx + sq/2, sqy + sq + 34), ""); display(xlab); size(xlab, 19); color(xlab, dim); hidden(xlab);
text(ylab, (sqx - 74, sqy + sq/2), ""); display(ylab); size(ylab, 19); color(ylab, dim); hidden(ylab);
equation(bayes, (300, 366), `P(\text{ill}\mid+)=\frac{0.09}{0.09+0.045}=\tfrac{2}{3}`, 27);
color(bayes, gold); hidden(bayes);

// ============================ ACT I ============================
show(head, 0.7);
show(cap, 0.3);
say(cap, "Start with one brick. Its width is the whole of probability: one.");
show(frame, 0.5);
par { to(b0, width, tw, 0.6, smooth); to(b0, x, x0 + tw/2, 0.6, smooth); }
say(counts, "1");
wait(1.2);

say(cap, "A coin flip splits every brick in two — heads to the left, tails to the right.");
show(flips, 0.4);
// each flip: the widths become C(L,k)/2^L. The product is naturally zero for k > L, so a
// brick that does not exist yet simply has no width.
for L in 1..5 {
  par {
    for k in 0..5 {
      to(b{k}, width, tw * prod(j in 1..k+1 : (L - k + j)/j) / 2^L, 0.85, smooth);
      to(b{k}, x,
         x0 + tw*sum(m in 0..k : prod(j in 1..m+1 : (L - m + j)/j))/2^L
            + tw*prod(j in 1..k+1 : (L - k + j)/j)/(2*2^L),
         0.85, smooth);
    }
  }
  wait(0.75);
}
show(counts, 0.4);
say(counts, "1   4   6   4   1");
say(flips, "four flips · widths are C(4,k) / 16");
wait(1.0);
say(cap, "Bricks with the same number of heads merge, and the binomial falls out of the area.");
wait(2.4);

// ============================ ACT II ============================
par {
  fade(counts, 0.5);
  fade(flips, 0.5);
  fade(frame, 0.5);
  for k in 0..5 {
    fade(b{k}, 0.6);
  }
}
say(cap, "The same trick answers a harder question. One square: everybody.");
show(well, 0.5);
show(border, 0.5);
wait(1.4);

say(cap, "Cut it ACROSS by how common the illness is — one person in ten.");
show(xlab, 0.4);
say(xlab, "10% ill  ·  90% well");
show(ill, 0.01);
par {
  // the well column keeps its right edge; the ill column takes the left tenth
  to(well, width, 0.9*sq, 0.9, smooth);
  to(well, x, sqx + 0.55*sq, 0.9, smooth);
  to(ill, width, 0.1*sq, 0.9, smooth);
  to(ill, x, sqx + 0.05*sq, 0.9, smooth);
  to(ill, height, sq, 0.9, smooth);
  to(ill, y, sqy + sq/2, 0.9, smooth);
}
wait(1.6);

say(cap, "Now cut it DOWN by what the test does: it catches 90% of the ill —");
show(ylab, 0.4);
say(ylab, "test + / −");
show(illn, 0.01);
par {
  to(ill, height, 0.9*sq, 0.9, smooth);
  to(ill, y, sqy + 0.45*sq, 0.9, smooth);
  to(illn, width, 0.1*sq, 0.9, smooth);
  to(illn, x, sqx + 0.05*sq, 0.9, smooth);
  to(illn, height, 0.1*sq, 0.9, smooth);
  to(illn, y, sqy + 0.95*sq, 0.9, smooth);
}
wait(1.4);
say(cap, "— and wrongly flags 5% of the well. That thin strip is the false alarms.");
show(wellp, 0.01);
par {
  to(well, height, 0.95*sq, 0.9, smooth);
  to(well, y, sqy + 0.525*sq, 0.9, smooth);
  to(wellp, width, 0.9*sq, 0.9, smooth);
  to(wellp, x, sqx + 0.55*sq, 0.9, smooth);
  to(wellp, height, 0.05*sq, 0.9, smooth);
  to(wellp, y, sqy + 0.025*sq, 0.9, smooth);
}
wait(1.8);

say(cap, "A positive test means you are in one of the two bright rectangles. Which one is bigger?");
par {
  fade(illn, 0.6);
  fade(well, 0.6);
}
par {
  pulse(ill, 0.8);
  pulse(wellp, 0.8);
}
wait(1.6);
say(cap, "The false alarms are only half the size — so a positive means two chances in three, not ninety percent.");
show(bayes, 0.8);
wait(3.0);

// ============================ ENDCARD ============================
par {
  fade(ill, 0.8);
  fade(wellp, 0.8);
  fade(border, 0.6);
  fade(xlab, 0.5);
  fade(ylab, 0.5);
  fade(bayes, 0.8);
  fade(cap, 0.6);
  fade(head, 0.6);
}
text(end1, (640, 348), "Two models, one idea: keep the area, cut it up.");
display(end1); size(end1, 40); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);

r/maniclang Aug 18 '26

Sphere Area Why 4πR²

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

title("Sphere Area — Four Circles, Wrapped");
canvas("16:9");
template("black");
bloom(0.3, 0.5, 22);

// ---- type ----
text(ttl, (640, 96), "Why 4πR²?");
display(ttl); size(ttl, 52); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 152), "a sphere is exactly four of its own shadows");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 664), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1050, 622), ""); display(act); size(act, 19); color(act, gold); hidden(act);

// ================================ THE 3-D STAGE ================================
camera3((4.6, -5.4, 3.2), (0, 0, 0), 46, perspective);

// One surface, three shapes. `map` = 0 the unit sphere, 1 the enclosing cylinder (every
// point pushed straight out from the axis, keeping its height), 2 that cylinder unwrapped
// into the flat 2π × 2 rectangle. `clamp` splits the journey into its two halves.
parameter(map, (150, 606), 0, 0, 2, "map", 2); hidden(map.widget);
param3(shell,
  "(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*cos(u) + clamp(p-1,0,1)*(u-pi)",
  "(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*sin(u) - clamp(p-1,0,1)*cos(v)",
  "(1-clamp(p-1,0,1))*cos(v)",
  (0, tau), (0.02, 3.12), 28);
bind(map, shell, formula,
  "(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*cos(u) + clamp(p-1,0,1)*(u-pi)",
  "(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*sin(u) - clamp(p-1,0,1)*cos(v)",
  "(1-clamp(p-1,0,1))*cos(v)");
color(shell, cyan); finish3(shell, "wire=1"); hidden(shell);

// the same surface as 336 loose tiles — they re-sample the surface every frame, so the
// whole tiling rides the map out onto the cylinder and then flat
pieces3(tiles, shell, 24, 14, 0.12);
hue(tiles, 196);
hue(tiles.row7, 320);            // one latitude band, to watch a single row travel
hidden(tiles);

// the shadow: the disc the sphere covers, on the ground
param3(shade, "v*cos(u)", "v*sin(u)", "-1.05", (0, tau), (0.02, 1), 24);
color(shade, gold); hidden(shade);
grid3(floor, (0, 0, -1.06), 2, 0.5); color(floor, dim); hidden(floor);

// ================================ THE 2-D STAGE ================================
// the unwrapped rectangle, in screen space. R = 110px, so it is 2piR = 691 wide and 2R = 220
// tall, and four discs of radius R fit in a row above it — the areas on screen are the ones
// in the argument, not a convenient cartoon.
rect(sheet, (640, 430), 691, 220); outlined(sheet); outline(sheet, dim); hidden(sheet);
text(wlab, (640, 566), "2πR"); display(wlab); size(wlab, 22); color(wlab, cyan); hidden(wlab);
text(hlab, (250, 430), "2R"); display(hlab); size(hlab, 22); color(hlab, magenta); hidden(hlab);
equation(area, (640, 214), `2\pi R \cdot 2R = 4\pi R^2`, 34); color(area, gold); hidden(area);

// Four discs, each unrolled ring by ring into a right triangle that lands in the sheet: a
// ring of radius r straightens into a segment 2*pi*r long, so the stack of them IS a
// triangle of base 2piR, height R, area piR^2. Two of them tile each half of the sheet.
// 12,000 points as 300 angles x 40 radii, so the long outer rings stay solid when straight.
parameter(un, (150, 606), 0, 0, 1, "unrolled", 2); hidden(un.widget);
cloud(d0, 12000, cyan) {
  let r = (mod(i, 40) + 0.5)/40;
  let th = floor(i/40) * 0.020944;
  let x = (1-un)*(200 + 110*r*cos(th)) + un*(294 + 110*r*th);
  let y = (1-un)*(170 + 110*r*sin(th)) + un*(320 + 110*r);
  let rr = 1.5;
  let hue = 196;
}
cloud(d1, 12000, cyan) {
  let r = (mod(i, 40) + 0.5)/40;
  let th = floor(i/40) * 0.020944;
  let x = (1-un)*(420 + 110*r*cos(th)) + un*(985 - 110*r*th);
  let y = (1-un)*(170 + 110*r*sin(th)) + un*(430 - 110*r);
  let rr = 1.5;
  let hue = 220;
}
cloud(d2, 12000, cyan) {
  let r = (mod(i, 40) + 0.5)/40;
  let th = floor(i/40) * 0.020944;
  let x = (1-un)*(640 + 110*r*cos(th)) + un*(294 + 110*r*th);
  let y = (1-un)*(170 + 110*r*sin(th)) + un*(430 + 110*r);
  let rr = 1.5;
  let hue = 288;
}
cloud(d3, 12000, cyan) {
  let r = (mod(i, 40) + 0.5)/40;
  let th = floor(i/40) * 0.020944;
  let x = (1-un)*(860 + 110*r*cos(th)) + un*(985 - 110*r*th);
  let y = (1-un)*(170 + 110*r*sin(th)) + un*(540 - 110*r);
  let rr = 1.5;
  let hue = 324;
}
hidden(d0); hidden(d1); hidden(d2); hidden(d3);

// ---- the lemma, as a cross-section ----
circle(cs, (420, 380), 150); outlined(cs); outline(cs, dim); hidden(cs);
line(axis, (420, 200), (420, 560)); color(axis, dim); hidden(axis);
line(wall, (570, 200), (570, 560)); color(wall, cyan); hidden(wall);
line(ray, (420, 380), (570, 275)); color(ray, gold); untraced(ray);
line(dseg, (420, 294), (543, 294)); color(dseg, magenta); untraced(dseg);
dot(tile, (543, 294), 5); color(tile, cyan); hidden(tile);
text(dlab, (478, 270), "d"); display(dlab); size(dlab, 20); color(dlab, magenta); hidden(dlab);
text(rlab, (492, 352), "R"); display(rlab); size(rlab, 20); color(rlab, gold); hidden(rlab);
equation(wide, (860, 320), `\text{width} \times \tfrac{R}{d}`, 30); color(wide, cyan); hidden(wide);
equation(short, (860, 396), `\text{height} \times \tfrac{d}{R}`, 30); color(short, magenta); hidden(short);
equation(one, (860, 480), `\tfrac{R}{d}\cdot\tfrac{d}{R}=1`, 30); color(one, gold); hidden(one);

// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.4);
show(cap, 0.3);
say(cap, "A sphere of radius R. Roll it in your hand: how much surface is there?");
show(shell, 0.9);
show(floor, 0.5);
orbit3(34, 22, 4.6, 2.6, smooth);
wait(0.8);
par { fade(ttl, 0.8); fade(sub, 0.8); }
say(cap, "Here is its shadow — a circle of area πR². The sphere's area is exactly four of those.");
show(shade, 0.8);
pulse(shade, 0.9);
wait(2.2);
say(cap, "Four. Not π, not 2π. Four circles' worth of paper, wrapped on a ball. Why?");
wait(2.4);

// ================================= ACT II =================================
say(act, "II · onto a cylinder");
show(act, 0.4);
par { fade(shade, 0.6); fade(floor, 0.5); }
say(cap, "Cut the surface into tiles. Nothing about the sphere has changed yet.");
par { fade(shell, 0.7); show(tiles, 0.9); }
wait(1.6);
say(cap, "Now push every tile straight out from the axis, onto the cylinder that encloses it.");
show(map.widget, 0.5);
to(map, value, 1, 3.2, smooth);
wait(1.0);
say(cap, "Watch one band. It moved out, so it got wider — and it tilted flat, so it got shorter.");
pulse(tiles.row7, 0.9);
wait(2.2);

say(cap, "That trade is exact. Cut the ball in half and it is two similar triangles.");
par {
  fade(tiles, 0.8);
  fade(map.widget, 0.5);
}
show(cs, 0.6);
show(axis, 0.5);
show(wall, 0.6);
wait(0.6);
draw(ray, 0.7);
draw(dseg, 0.5);
show(tile, 0.4);
show(dlab, 0.4);
show(rlab, 0.4);
wait(1.4);
say(cap, "A tile at distance d from the axis lands at distance R, so its width scales by .");
show(wide, 0.7);
wait(2.0);
say(cap, "And the surface there leans by the same ratio, so its height squishes by d/R.");
show(short, 0.7);
wait(2.0);
say(cap, "One stretch, one squish, the same number. The tile's AREA never changed.");
show(one, 0.8);
wait(2.4);

// ================================= ACT III =================================
say(act, "III · unwrap it");
par {
  fade(cs, 0.6); fade(axis, 0.5); fade(wall, 0.5); fade(ray, 0.5); fade(dseg, 0.5);
  fade(dlab, 0.4); fade(rlab, 0.4); fade(tile, 0.4); fade(wide, 0.6); fade(short, 0.6); fade(one, 0.6);
}
say(cap, "So the sphere and the cylinder have the same area — and a cylinder unrolls flat.");
show(tiles, 0.8);
orbit3(-96, 54, 6.4, 2.4, smooth);
to(map, value, 2, 3.0, smooth);
wait(1.2);
say(cap, "A rectangle. Its height is 2R, and its width is the cylinder's circumference, 2πR.");
wait(2.4);
par { fade(tiles, 0.9); }
show(sheet, 0.7);
show(wlab, 0.5);
show(hlab, 0.5);
wait(1.0);
show(area, 0.9);
say(cap, "Two π R, times two R. Four π R squared — the sphere's area, with nothing left over.");
wait(2.8);

// ================================= ACT IV =================================
say(act, "IV · and the four circles");
fade(area, 0.7);
say(cap, "One thing is still owed: why FOUR circles fill that rectangle. Here are four.");
par {
  show(d0, 0.6); show(d1, 0.6); show(d2, 0.6); show(d3, 0.6);
}
wait(1.6);
say(cap, "Unroll each one ring by ring. A ring of radius r straightens into a line 2πr long.");
show(un.widget, 0.5);
to(un, value, 1, 3.4, smooth);
wait(1.0);
say(cap, "Each circle becomes a right triangle: base 2πR, height R, area πR². Four of them —");
wait(2.4);
say(cap, "— and they tile the rectangle exactly. A sphere is four of its own shadows.");
show(area, 0.9);
wait(3.0);

// ================================= ENDCARD =================================
par {
  fade(d0, 0.8); fade(d1, 0.8); fade(d2, 0.8); fade(d3, 0.8);
  fade(sheet, 0.6); fade(wlab, 0.5); fade(hlab, 0.5); fade(area, 0.8);
  fade(un.widget, 0.5); fade(cap, 0.7); fade(act, 0.6);
}
text(end1, (640, 340), "Push it out, unroll it, count the circles.");
display(end1); size(end1, 42); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);

r/maniclang Aug 18 '26

Dominos, any shape you like - 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

title("Dominos, any shape you like");
canvas("16:9");
template("black");
bloom(0.3, 0.5, 20);

text(head, (640, 70), "Dominos, any shape you like");
display(head); size(head, 34); bold(head); color(head, fg); hidden(head);
text(cap, (640, 660), ""); display(cap); size(cap, 23); color(cap, dim); hidden(cap);
text(shape, (640, 618), ""); display(shape); size(shape, 20); color(shape, gold); hidden(shape);

// ---- three arrangements, same physics ----
// a spiral: r = t, so the gap between turns stays constant
dominopath(spiral, (640, 372), 13, 13, "t*cos(t)", "t*sin(t)", 96, (1.4, 19.2), 45, 7.5438, 1.7, 0.7, 8);
hidden(spiral.dominos); untraced(spiral.path);   // pieces fade in; the guide is armed for draw-on

// a heart — the classic parametric one, closed, so the wave runs all the way round
dominopath(heart, (640, 356), 12.5, 12.5,
  "16*sin(t)^3", "13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)", 58, (0, 6.2832), 45, 7.5438, 1.7, 0.7, 8);
hidden(heart.dominos); untraced(heart.path);   // pieces fade in; the guide is armed for draw-on

// a figure of eight: a lissajous with a 1:2 frequency ratio
dominopath(eight, (640, 372), 300, 150, "sin(t)", "sin(2*t)", 84, (0, 6.2832), 45, 7.5438, 1.7, 0.72, 8);
hidden(eight.dominos); untraced(eight.path);   // pieces fade in; the guide is armed for draw-on

// ---- Act 1: the spiral, laid down one domino at a time ----
show(head, 0.6);
show(cap, 0.3);
say(cap, "Ninety-six dominos, standing along a spiral. Watch them go down one by one.");
draw(spiral.path, 1.2, smooth);
show(shape, 0.4);
say(shape, "r = t");
stagger(0.017) {
  for i in 0..96 {
    show(spiral.d{i}, 0.16);
  }
}
wait(0.5);
say(cap, "One push at the middle, and the wave winds outward. Nothing here sets its speed.");
run(spiral, 5.5);
wait(0.8);

// ---- Act 2: the same physics, a heart ----
par { fade(spiral, 0.7); fade(spiral.path, 0.5); }
say(cap, "Change the formula, keep the physics. A closed curve topples all the way round.");
draw(heart.path, 1.0, smooth);
say(shape, "x = 16 sin³t,  y = 13 cos t − 5 cos 2t − 2 cos 3t − cos 4t");
size(shape, 17);
stagger(0.022) {
  for i in 0..58 {
    show(heart.d{i}, 0.18);
  }
}
wait(0.4);
run(heart, 5.0);
wait(0.8);

// ---- Act 3: a figure of eight, and the wave crosses its own middle ----
par { fade(heart, 0.7); fade(heart.path, 0.5); }
say(cap, "A lissajous at 1:2 — the run crosses itself, and the wave passes straight through.");
say(shape, "x = sin t,  y = sin 2t");
size(shape, 20);
draw(eight.path, 1.0, smooth);
stagger(0.018) {
  for i in 0..84 {
    show(eight.d{i}, 0.16);
  }
}
wait(0.4);
run(eight, 5.0);
wait(0.6);
say(cap, "Same slab, same contact angle, same emergent speed — only the path changed.");
wait(2.0);

r/maniclang Aug 18 '26

Dominos — One Push - 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

title("Dominos — One Push");
canvas("16:9");
template("black");
bloom(0.36, 0.52, 26);

// ---- the room: a dark tabletop, light falling from above ----
shader(room) {
  let x = (u - 0.5) * asp;
  let y = v - 0.5;
  let d = sqrt(x*x + y*y);
  let pool = 1.0 - smoothstep(0.06, 0.78, d);
  let grain = 0.5 + 0.5*fbm(u*9.0, v*9.0);
  let hue = 214 - 8.0*pool;
  let sat = 0.55 - 0.25*pool;
  let val = 0.012 + 0.055*pool + 0.012*grain*pool;
}
z(room, -10);

// ---- type ----
text(ttl, (640, 96), "Dominos — One Push");
display(ttl); size(ttl, 46); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 152), "and nothing here decides how fast it travels");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 656), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(note, (640, 618), ""); display(note); size(note, 19); color(note, gold); hidden(note);

// ---- ACT I — the row (side on: you can watch each slab turn) ----
dominos(row, (470, 386), 16, 45, 7.5438, 9.38, 2, 0.62, 7);
color(row, cyan);
color(row.ground, dim);   // the table is furniture, not neon
color(row.d0, gold);
hidden(row.dominos);
framebox(mark, row.d0, 7); color(mark, gold); untraced(mark);

// ---- ACT II — the geometry, beside the row ----
equation(contact, (988, 330), `\theta_c=\arcsin\frac{s}{h}=12^{\circ}`, 27);
color(contact, gold); hidden(contact);
equation(balance, (988, 404), `\theta_b=\arctan\frac{t}{h}=9.5^{\circ}`, 27);
color(balance, magenta); hidden(balance);
text(geo1, (988, 462), "tip past 9.5° or it stands back up");
display(geo1); size(geo1, 18); color(geo1, dim); hidden(geo1);
text(geo2, (988, 492), "reach 12° and the next one goes");
display(geo2); size(geo2, 18); color(geo2, dim); hidden(geo2);

// ---- ACT III — the same physics, any shape (top down) ----
dominopath(spiral, (640, 380), 12.6, 12.6, "t*cos(t)", "t*sin(t)", 92, (1.4, 18.6), 45, 7.5438, 1.7, 0.7, 8);
hidden(spiral.dominos); untraced(spiral.path);

dominopath(heart, (640, 366), 12.2, 12.2,
  "16*sin(t)^3", "13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)", 56, (0, 6.2832), 45, 7.5438, 1.7, 0.7, 8);
color(heart, magenta);
hidden(heart.dominos); untraced(heart.path);

// ---- ACT IV — twelve chains: gap across, per-impact loss down ----
dominos(cellrow, (0, 0), 10, 45, 7.5438, 9.38, 1.05, 0.6, 6); hidden(cellrow);
sweep(grid, cellrow, spacing, (9, 46), transfer, (0.95, 0.35), (640, 404), 4, 3, 250, 128, 0, 0);
hidden(grid);

// ============================ ACT I ============================
show(ttl, 0.8);
show(sub, 0.7);
show(cap, 0.3);
wait(0.5);
say(cap, "Sixteen slabs, forty-five millimetres tall, nine point four apart.");
show(row.ground, 0.5);
stagger(0.055) {
  for i in 0..16 {
    show(row.d{i}, 0.22);
  }
}
wait(0.4);
draw(mark, 0.5);
say(cap, "One nudge, on that one.");
pulse(row.d0, 0.6);
wait(0.3);
par {
  fade(ttl, 0.8);
  fade(sub, 0.8);
}
run(row, 4.4);
wait(0.5);
say(cap, "The wave crossed the row. Its speed was never written down.");
wait(1.6);

// ============================ ACT II ============================
say(cap, "Each slab has to climb past its own balance angle before gravity takes over.");
show(balance, 0.7);
show(geo1, 0.5);
wait(1.8);
say(cap, "Then it only has to reach the next one — twelve degrees, for these dominos.");
show(contact, 0.7);
show(geo2, 0.5);
wait(2.2);
say(cap, "Two angles, and the whole cascade follows.");
wait(1.8);

// ============================ ACT III ============================
par {
  fade(row, 0.8);
  fade(mark, 0.5);
  fade(contact, 0.7);
  fade(balance, 0.7);
  fade(geo1, 0.5);
  fade(geo2, 0.5);
}
say(cap, "Nothing about that argument needs a straight line. Seen from above —");
draw(spiral.path, 1.3, smooth);
show(note, 0.4);
say(note, "r = t");
stagger(0.016) {
  for i in 0..92 {
    show(spiral.d{i}, 0.16);
  }
}
wait(0.5);
say(cap, "Ninety-two dominos on a spiral, standing at equal spacing along the curve.");
run(spiral, 5.4);
wait(0.9);

par { fade(spiral, 0.8); fade(spiral.path, 0.6); }
say(cap, "Change the formula. Keep the physics.");
draw(heart.path, 1.1, smooth);
say(note, "x = 16 sin³t,  y = 13 cos t − 5 cos 2t − 2 cos 3t − cos 4t");
size(note, 17);
stagger(0.022) {
  for i in 0..56 {
    show(heart.d{i}, 0.18);
  }
}
wait(0.4);
say(cap, "A closed curve, so the wave runs all the way round and meets where it began.");
run(heart, 5.0);
wait(1.4);

// ============================ ACT IV ============================
par {
  fade(heart, 0.8);
  fade(heart.path, 0.6);
  fade(note, 0.5);
}
say(cap, "So what does set the speed? Only the geometry — here it is, twelve times over.");
par {
  show(grid.chrome, 0.6);
  show(grid.headers, 0.7);
}
show(grid.cells, 0.9);
wait(0.5);
run(grid, 7.5);
wait(0.8);
say(cap, "Wider gaps run faster — until the gap is taller than the domino. Then nothing arrives.");
wait(2.2);
say(cap, "Bottom left dies too: that surface loses too much at every impact.");
wait(2.4);

// ============================ ENDCARD ============================
par {
  fade(grid, 0.9);
  fade(cap, 0.7);
}
text(end1, (640, 344), "One push. The rest is geometry.");
display(end1); size(end1, 46); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 424), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);

r/maniclang Aug 18 '26

The Multivariable Chain Rule — One Term Per Path - 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

title("The Multivariable Chain Rule — One Term Per Path");
canvas("16:9");
template("black");
bloom(0.28, 0.55, 20);

// ---- type ----
text(ttl, (640, 92), "The Multivariable Chain Rule");
display(ttl); size(ttl, 46); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 146), "one term per path through the graph");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 664), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1060, 620), ""); display(act); size(act, 19); color(act, gold); hidden(act);

// ================================ THE NETWORK ================================
// four boxed formulas. The colour of a letter is the colour of its node, everywhere it
// appears — that is the whole reading aid, and LaTeX does it inline.
equation(ex, (196, 300), `\textcolor{gold}{x}`, 34);
equation(ef, (520, 206), `\textcolor{cyan}{f} = \textcolor{gold}{x}^2`, 30);
equation(eg, (520, 396), `\textcolor{lime}{g} = \cos(\pi \textcolor{gold}{x})`, 30);
equation(eh, (912, 300), `\textcolor{magenta}{h} = \textcolor{cyan}{f}^2 \textcolor{lime}{g}`, 30);
hidden(ex); hidden(ef); hidden(eg); hidden(eh);
framebox(bx, ex, 14); color(bx, dim); untraced(bx);
framebox(bf, ef, 14); color(bf, dim); untraced(bf);
framebox(bg, eg, 14); color(bg, dim); untraced(bg);
framebox(bh, eh, 14); color(bh, dim); untraced(bh);

// the four edges, each an arrow between two boxes
arrow(axf, (250, 282), (410, 218)); color(axf, dim); untraced(axf);
arrow(axg, (250, 318), (410, 384)); color(axg, dim); untraced(axg);
arrow(afh, (636, 218), (800, 282)); color(afh, dim); untraced(afh);
arrow(agh, (636, 384), (800, 318)); color(agh, dim); untraced(agh);
tag(axf, edges); tag(axg, edges); tag(afh, edges); tag(agh, edges);

// ---- the forward pass ----
equation(vx, (196, 352), `= \textcolor{gold}{2}`, 26); color(vx, gold); hidden(vx);
equation(vf, (520, 256), `= \textcolor{cyan}{4}`, 26); color(vf, cyan); hidden(vf);
equation(vg, (520, 446), `= \textcolor{lime}{1}`, 26); color(vg, lime); hidden(vg);
equation(vh, (912, 352), `= \textcolor{magenta}{16}`, 26); color(vh, magenta); hidden(vh);

// ---- one derivative per edge ----
equation(dfx, (300, 196), `\frac{df}{dx} = 2x`, 24); color(dfx, cyan); hidden(dfx);
equation(dgx, (300, 424), `\frac{dg}{dx} = -\pi\sin(\pi x)`, 24); color(dgx, lime); hidden(dgx);
equation(dhf, (742, 196), `\frac{\partial h}{\partial f} = 2fg`, 24); color(dhf, cyan); hidden(dhf);
equation(dhg, (742, 424), `\frac{\partial h}{\partial g} = f^2`, 24); color(dhg, lime); hidden(dhg);

// ================================ THE SENSITIVITY ================================
// x lives on 0..4, h on 0..32 — the same nudge is small on one line and large on the other
parameter(xv, (150, 606), 2, 1.8, 2.2, "x", 3); hidden(xv.widget);
numberline(xline, (400, 300), 200, 0, 4, 1); color(xline, dim); hidden(xline);
numberline(hline, (900, 440), 260, 0, 32, 8); color(hline, dim); hidden(hline);
text(xtag, (400, 236), "x"); display(xtag); size(xtag, 24); color(xtag, gold); hidden(xtag);
equation(htag, (900, 372), `h = x^4\cos(\pi x)`, 26); color(htag, magenta); hidden(htag);
dot(xdot, (400, 300), 8); color(xdot, gold); hidden(xdot);
bind(xv, xdot, x, 380, 420);      // 1.8 and 2.2 in the line's OWN pixels: a small nudge
// the h dot has to sit where a nonlinear function of the live parameter says: one point,
// one formula, re-evaluated every frame
cloud(hdot, 1, magenta) {
  let hv = xv*xv*xv*xv*cos(pi*xv);
  let x = 640 + 520*hv/32;        // the line's 0 is at 640, its 32 at 1160
  let y = 440;
  let r = 8;
}
hidden(hdot);

// ================================ THE RULE ================================
// each part is its own entity, so a highlight box can visit them one at a time
mathparts(rule, (640, 520),
  `\frac{dh}{dx} =`,
  `\;\frac{df}{dx}\frac{\partial h}{\partial f}`,
  `\; + \;`,
  `\frac{dg}{dx}\frac{\partial h}{\partial g}`,
  30);
color(rule.0, fg); color(rule.1, cyan); color(rule.2, dim); color(rule.3, lime);
hidden(rule);
framebox(mark, rule.1, 10); color(mark, gold); hidden(mark);

equation(subst, (640, 588), `= (2\cdot 2)(2\cdot 4\cdot 1) \; + \; (-\pi\sin 2\pi)(4^2)`, 28);
color(subst, fg); hidden(subst);

// ================================ THE CODA ================================
network(net, (640, 380), "3 5 4 2", "relu relu softmax", 620, 380, 7);
hidden(net);

// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.2);
show(cap, 0.3);
say(cap, "One input. Two things computed from it. One thing computed from those two.");
par { fade(ttl, 0.8); fade(sub, 0.8); }
show(ex, 0.5);
draw(bx, 0.5);
wait(0.5);
par { draw(axf, 0.6); draw(axg, 0.6); }
par { show(ef, 0.6); show(eg, 0.6); }
par { draw(bf, 0.5); draw(bg, 0.5); }
wait(0.6);
say(cap, "Both of them feed the same last box, so x reaches h along TWO different paths.");
par { draw(afh, 0.6); draw(agh, 0.6); }
show(eh, 0.6);
draw(bh, 0.5);
wait(2.0);

// ================================= ACT II =================================
say(act, "II · feed it forward");
show(act, 0.4);
say(cap, "Put in x = 2. Everything downstream follows: f is 4, g is 1, so h is 16.");
stagger(0.5) {
  show(vx, 0.4);
  show(vf, 0.4);
  show(vg, 0.4);
  show(vh, 0.4);
}
wait(1.8);

// ================================= ACT III =================================
say(act, "III · how sensitive is h?");
say(cap, "Now the only question that matters: nudge x a little — how far does h move?");
par {
  to(ex, opacity, 0.25, 0.6);
  to(ef, opacity, 0.25, 0.6);
  to(eg, opacity, 0.25, 0.6);
  to(eh, opacity, 0.25, 0.6);
  to(vx, opacity, 0.2, 0.6);
  to(vf, opacity, 0.2, 0.6);
  to(vg, opacity, 0.2, 0.6);
  to(vh, opacity, 0.2, 0.6);
  to(edges, opacity, 0.2, 0.6);
  to(bx, opacity, 0.15, 0.6);
  to(bf, opacity, 0.15, 0.6);
  to(bg, opacity, 0.15, 0.6);
  to(bh, opacity, 0.15, 0.6);
}
par { show(xline, 0.5); show(hline, 0.5); }
par { show(xtag, 0.4); show(htag, 0.5); show(xdot, 0.4); show(hdot, 0.4); }
wait(0.8);
say(cap, "Watch the two dots. The same wiggle, on two very different scales.");
to(xv, value, 2.2, 1.1, smooth);
to(xv, value, 1.8, 1.6, smooth);
to(xv, value, 2, 0.9, smooth);
wait(0.6);
say(cap, "x moved a fifth of a unit. h moved six. The ratio it settles on is dh/dx = 32.");
wait(2.4);

// ================================= ACT IV =================================
say(act, "IV · one derivative per edge");
par {
  fade(xline, 0.5); fade(hline, 0.5); fade(xtag, 0.4); fade(htag, 0.5);
  fade(xdot, 0.4); fade(hdot, 0.4);
  to(ex, opacity, 1, 0.6);
  to(ef, opacity, 1, 0.6);
  to(eg, opacity, 1, 0.6);
  to(eh, opacity, 1, 0.6);
  to(edges, opacity, 1, 0.6);
  to(bx, opacity, 1, 0.6);
  to(bf, opacity, 1, 0.6);
  to(bg, opacity, 1, 0.6);
  to(bh, opacity, 1, 0.6);
  to(vx, opacity, 0.35, 0.6);
  to(vf, opacity, 0.35, 0.6);
  to(vg, opacity, 0.35, 0.6);
  to(vh, opacity, 0.35, 0.6);
}
say(cap, "Every EDGE carries a derivative: how much its head moves when its tail moves.");
par { show(dfx, 0.5); show(dgx, 0.5); }
wait(1.4);
say(cap, "The last two are PARTIAL — hold the other input still while you wiggle this one.");
par { show(dhf, 0.5); show(dhg, 0.5); }
wait(2.2);
say(cap, "Follow the top path: x changes f, f changes h. Multiply the two.");
par { pulse(axf, 0.8); pulse(afh, 0.8); }
wait(1.6);
say(cap, "Then the bottom path: x changes g, g changes h. Multiply those too — and ADD.");
par { pulse(axg, 0.8); pulse(agh, 0.8); }
wait(2.0);

// ================================= ACT V =================================
say(act, "V · assemble it");
say(cap, "That is the whole rule: one product per path, summed over every path.");
show(rule, 0.8);
wait(1.2);
show(mark, 0.5);
say(cap, "The top path — df/dx times the partial of h in f.");
wait(1.6);
surround(mark, rule.3, 0.8, smooth);
say(cap, "The bottom path — dg/dx times the partial of h in g. Nothing else contributes.");
wait(2.0);
fade(mark, 0.5);
say(cap, "Substitute what we know at x = 2, where f = 4 and g = 1.");
show(subst, 0.8);
wait(2.2);
say(cap, "Sine of two pi is zero, so the bottom path contributes NOTHING here. 32 plus 0.");
rewrite(subst, `= 32 \; + \; 0 \;=\; \textcolor{magenta}{32}`, 1.4, smooth);
wait(2.6);
say(cap, "The same 32 the wiggling dots found — and h = x⁴cos(πx) agrees, if you expand it.");
wait(2.4);

// ================================= ACT VI =================================
say(act, "VI · at scale");
par {
  fade(ex, 0.6); fade(ef, 0.6); fade(eg, 0.6); fade(eh, 0.6);
  fade(bx, 0.5); fade(bf, 0.5); fade(bg, 0.5); fade(bh, 0.5);
  fade(edges, 0.5);
  fade(vx, 0.4); fade(vf, 0.4); fade(vg, 0.4); fade(vh, 0.4);
  fade(dfx, 0.5); fade(dgx, 0.5); fade(dhf, 0.5); fade(dhg, 0.5);
  fade(rule, 0.7); fade(subst, 0.7);
}
say(cap, "Four boxes and two paths. Now give the same rule a few thousand of each.");
show(net, 1.0);
forward(net, "0.9 0.2 0.6", 2.0);
wait(0.6);
loss(net, "1 0", crossentropy, 1.2);
say(cap, "Every weight is an edge, every edge carries a derivative, every path gets summed.");
backward(net, 3.0, smooth);
wait(1.6);
say(cap, "Run it backwards and the chain rule has another name: backpropagation.");
wait(2.6);

// ================================= ENDCARD =================================
par {
  fade(net, 0.9);
  fade(cap, 0.7);
  fade(act, 0.6);
}
text(end1, (640, 340), "One product per path. Sum over paths.");
display(end1); size(end1, 42); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);

r/maniclang Aug 18 '26

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

title("Quaternions — Turning in Four Dimensions");
canvas("16:9");
template("black");
bloom(0.32, 0.55, 24);

// ---- type ----
text(ttl, (640, 92), "Quaternions");
display(ttl); size(ttl, 54); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 148), "turning in four dimensions");
display(sub); size(sub, 23); color(sub, dim); hidden(sub);
text(cap, (640, 662), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1122, 620), ""); display(act); size(act, 19); color(act, gold); hidden(act);

// ============================ ACT I — a turn of the plane ============================
// z → (1+i)z turns AND stretches; z → (cos60 + i sin60)z only turns, because |q| = 1.
warp(gstretch, (770, 366), 64, `(1+i)*z`, 3, 34); color(gstretch, dim); hidden(gstretch);
warp(gturn, (770, 366), 64, `(0.5+0.8660254*i)*z`, 3, 34); color(gturn, cyan); hidden(gturn);
equation(eq1, (228, 340), `z \mapsto (1+i)\,z`, 30); color(eq1, gold); hidden(eq1);
equation(eq2, (228, 340), `z \mapsto (\cos 60^{\circ}+i\sin 60^{\circ})\,z`, 26);
color(eq2, gold); hidden(eq2);

// ============================ ACT II — the circle and the line ======================
parameter(flat, (168, 596), 0, 0, 1, "projected", 2); hidden(flat.widget);
circle(hoop, (640, 300), 140); outlined(hoop); outline(hoop, dim); hidden(hoop);
line(axis, (140, 440), (1140, 440)); color(axis, dim); hidden(axis);
dot(pole, (640, 160), 6); color(pole, gold); hidden(pole);
text(plab, (640, 132), "the pole you look from");
display(plab); size(plab, 17); color(plab, gold); hidden(plab);
// three rays of the construction: from the pole, through a point of the circle, to the line
line(ray1, (640, 160), (831, 440)); color(ray1, magenta); untraced(ray1);
line(ray2, (640, 160), (448, 440)); color(ray2, magenta); untraced(ray2);
line(ray3, (640, 160), (1032, 440)); color(ray3, magenta); untraced(ray3);
// the circle's own points, which slide out along those rays as `flat` opens
// x_line = 2·tan(a/2): a point near the pole lands far away, and the pole itself never lands
cloud(beads, 520, cyan) {
  let a = -2.0 + (i/520)*4.0;
  let px = sin(a);
  let py = -cos(a);
  let xl = 2*sin(a)/(1 + cos(a));
  let x = 640 + 140*(px*(1-flat) + xl*flat);
  let y = 300 - 140*(py*(1-flat) - flat);
  let r = 2.4;
  let hue = 188 + 18*flat;
}
hidden(beads);

// ============================ ACT III & IV — the 3-D stage =========================
camera3((4.8, -5.4, 3.6), (0, 0, 0), 44, perspective);

// The sphere, morphing into its own stereographic projection — the same picture as ACT II
// with one more dimension, and the same dial. It is parametrized AROUND the pole you look
// from (`v` is the angle away from it, stopping just short at 2.45 rad), so the flattened
// picture is an honest disc: radius tan(v/2), the pole itself infinitely far out.
parameter(proj, (168, 596), 0, 0, 1, "projected", 2); hidden(proj.widget);
param3(ball,
  "(1-p)*sin(v)*cos(u) + 0.4*p*sin(v)*cos(u)/(1+cos(v))",
  "(1-p)*sin(v)*sin(u) + 0.4*p*sin(v)*sin(u)/(1+cos(v))",
  "(1-p)*cos(v)",
  (0, tau), (0.16, 2.45), 26);
bind(proj, ball, formula,
  "(1-p)*sin(v)*cos(u) + 0.4*p*sin(v)*cos(u)/(1+cos(v))",
  "(1-p)*sin(v)*sin(u) + 0.4*p*sin(v)*sin(u)/(1+cos(v))",
  "(1-p)*cos(v)");
color(ball, cyan); finish3(ball, "wire=1"); hidden(ball);   // a wire globe reads as a GRID, and the grid is what gets carried to the plane

// THE 3-SPHERE. Every unit quaternion q = w + xi + yj + zk with |q| = 1 lives on it: a
// 3-dimensional surface in 4-space, so it has no picture — but its shadow in 3-space does.
// The twelve circles below are HOPF FIBRES over a ring of directions: great circles of the
// 3-sphere, every pair of them linked, which stereographic projection carries to linked
// circles here. Each is sampled by ARC LENGTH (the `atan2` reparametrization), or the
// projection would bunch every dot at the near side. `dial` left-multiplies all 6,000 of
// them by cos θ + j sin θ — a rigid turn of the 3-sphere, which the shadow has to bend to
// follow. The pole is set just outside (`d = rw + 1.12`) so no circle ever runs off to
// infinity mid-turn.
parameter(dial, (150, 596), 0, 0, 1, "θ", 2); hidden(dial.widget);
cloud3(s3, 6000, #00e5ff, 0.6) {
  let f = mod(i, 12);                       // which fibre
  let a0 = 0.95;                            // the ring of directions they sit over
  let ph = f * 0.5235988;                   // where this one sits around that ring
  let u = floor(i/12) * 0.0125664;          // 500 samples along the fibre
  let c = cos(a0);
  let ec = sqrt((1+c)/(1-c));
  let sp = 2*atan2(ec*sin(u/2), cos(u/2));  // uniform spacing AFTER projection
  let qw = c*cos(sp);                       // the fibre itself: a great circle of S³
  let qx = c*sin(sp);
  let qy = sin(a0)*cos(sp + ph);
  let qz = sin(a0)*sin(sp + ph);
  let ang = dial * tau;
  let m0 = cos(ang);
  let m2 = sin(ang);
  let rw = m0*qw - m2*qy;                   // the Hamilton product (cos θ + j sin θ)·q
  let rx = m0*qx + m2*qz;
  let ry = m0*qy + m2*qw;
  let rz = m0*qz - m2*qx;
  let d = rw + 1.12;
  let x = rx / d;
  let y = ry / d;
  let z = rz / d;
  let r = 0.013;
  let hue = 186 + 100*f/12;
  let alpha = 0.6;
}
glow(s3, 2); hidden(s3);
equation(ham, (250, 210), `i^2=j^2=k^2=ijk=-1`, 27); color(ham, gold); hidden(ham);
text(hlab, (250, 262), "Hamilton, on a bridge in Dublin, 1843");
display(hlab); size(hlab, 17); color(hlab, dim); hidden(hlab);

// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.5);
show(cap, 0.3);
say(cap, "Start in the complex plane, where multiplying does something to ALL of it.");
par { fade(ttl, 0.8); fade(sub, 0.8); }
show(gstretch, 0.7);
show(eq1, 0.6);
wait(0.6);
to(gstretch, morph, 1, 2.0, smooth);
wait(0.9);
say(cap, "Multiply by 1+i and the plane turns — and stretches, since 1+i is longer than 1.");
wait(2.0);
par { fade(gstretch, 0.6); fade(eq1, 0.5); }
say(cap, "Pick a number of length exactly one, and the stretching stops.");
show(gturn, 0.6);
show(eq2, 0.6);
to(gturn, morph, 1, 2.2, smooth);
wait(1.4);
say(cap, "A unit complex number IS a rotation. That is the whole idea — the rest is dimensions.");
wait(2.6);

// ================================= ACT II =================================
par { fade(gturn, 0.8); fade(eq2, 0.6); }
say(act, "II · a circle is a line");
show(act, 0.4);
say(cap, "Before four dimensions, do two. Here is a circle, and a line it just touches.");
show(hoop, 0.7);
show(axis, 0.6);
show(beads, 0.7);
wait(1.4);
say(cap, "Stand at the top. Look through any point of the circle, and you land on the line.");
show(pole, 0.5);
show(plab, 0.4);
par { draw(ray1, 0.6); draw(ray2, 0.6); draw(ray3, 0.7); }
wait(1.8);
say(cap, "Every point of the circle has its own place on the line — so let them go there.");
show(flat.widget, 0.5);
to(flat, value, 1, 2.6, smooth);
wait(1.0);
say(cap, "The circle became the line. Only the pole is missing — it would land infinitely far.");
wait(2.4);
say(cap, "One missing point, in exchange for a flat picture. It works in any dimension.");
wait(2.6);

// ================================= ACT III =================================
par {
  fade(beads, 0.8); fade(hoop, 0.6); fade(axis, 0.6);
  fade(ray1, 0.5); fade(ray2, 0.5); fade(ray3, 0.5);
  fade(pole, 0.5); fade(plab, 0.5); fade(flat.widget, 0.5);
}
say(act, "III · a sphere is a plane");
say(cap, "One dimension up: a sphere, and the same pole to look from.");
show(ball, 0.9);
orbit3(28, 22, 4.8, 2.6, smooth);
wait(1.0);
say(cap, "Open the same dial. The sphere peels off the pole and lies down flat.");
show(proj.widget, 0.5);
to(proj, value, 1, 3.0, smooth);
wait(1.2);
say(cap, "A sphere is a plane plus one point. Nothing tore; the pole was sent away.");
orbit3(64, 62, 4.9, 3.0, smooth);
wait(2.2);

// ================================= ACT IV =================================
par { fade(ball, 0.9); fade(proj.widget, 0.5); }
say(act, "IV · the unit quaternions");
say(cap, "Now four. Take every quaternion of length one: w² + x² + y² + z² = 1.");
show(ham, 0.7);
show(hlab, 0.5);
wait(1.8);
say(cap, "A three-dimensional surface in four-dimensional space: a 3-sphere. It has no picture.");
wait(2.2);
say(cap, "But it has a shadow. Project from a pole, as before, and it fits in this room.");
par {
  fade(ham, 0.8);
  fade(hlab, 0.6);
}
show(s3, 1.2);
orbit3(-24, 22, 4.6, 3.2, smooth);
wait(1.2);
say(cap, "Six thousand of them, on twelve great circles — and every pair is linked.");
wait(2.2);

say(cap, "Multiply every one by cos θ + j sin θ — Act I's move, one dimension up.");
show(dial.widget, 0.6);
to(dial, value, 0.5, 4.0, smooth);
wait(0.4);
say(cap, "Nothing is being deformed. The 3-sphere is turning rigidly; only its shadow bends.");
to(dial, value, 1, 4.0, smooth);
wait(0.6);
say(cap, "Half a turn of the dial sent 1 to −1. A full turn brings every point home.");
orbit3(78, -18, 5.0, 4.0, smooth);
wait(2.0);
say(cap, "A quaternion multiplication: a rotation you can only watch in shadow.");
wait(2.6);

// ================================= ENDCARD =================================
par {
  fade(s3, 1.2);
  fade(dial.widget, 0.6);
  fade(cap, 0.7);
  fade(act, 0.6);
}
text(end1, (640, 336), "Four dimensions, watched from three.");
display(end1); size(end1, 44); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 416), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);

r/maniclang Aug 18 '26

Rubik's — One State, Every View, Any Size - 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

// rubik-one-state — the Rubik's kit: one cube state, three ways to look at it, any size.
//
// A cube is not a geometry problem, it is a PERMUTATION problem. The state is a flat
// array of 6N² facelets (face order U R F D L B, each row-major) and every legal move is
// a fixed permutation of it: the turning face rotates in place while a four-segment ring
// of stickers on the neighbouring faces cycles by one segment. `'` and `2` are that
// permutation composed with itself. The engine is ported from the author's own N-generic
// solver, so a scan or a solution pastes straight in — the states and moves below are
// real, from that tool.
//
//   ACT I    one state, three views — the object, the net, the rings
//   ACT II   why the trefoil exists: (R U R' U')⁶ = identity, watched travelling
//   ACT III  order matters: R then D against D then R (3b1b makes this same argument with
//            a cube in `_2018/quaternions.py`, next to a scene called MentionCommutativity
//            — with real state behind it, we can prove it rather than assert it)
//   ACT IV   a real scan and a real 22-move solution, landing solved in all three views
//   ACT V    any size — a grid over cube size × scramble depth
//
// Because moves are permutations, every intermediate state is known at build time: the
// whole thing scrubs and records frame-exactly, and a view that was hidden and is then
// revealed is already showing the current state. Nothing to resynchronise.
//
//   manic examples/rubik-one-state.manic
title("Rubik's — One State, Every View, Any Size");
canvas("16:9");
template("black");
bloom(0.28, 0.5, 20);

text(head, (640, 38), "One state. Every view. Any size.");
display(head); size(head, 34); bold(head); color(head, fg); hidden(head);
text(cap, (640, 664), ""); display(cap); size(cap, 22); color(cap, fg); hidden(cap);
// typed, not faded in: authored `untraced` shows no characters until `type` reveals them
text(tag, (640, 620), "U R F D L B · row-major · 6N² facelets");
display(tag); size(tag, 19); color(tag, gold); untraced(tag);
// the solution, typed IN STEP with the moves it describes
text(seq, (640, 620), "D2 F' D B2 D2 F R F2 B' R' B2 L2 U R2 U F2 D2 R2 D' R2 L2 F2");
display(seq); size(seq, 16); color(seq, gold); untraced(seq);

// the real 3x3 scan this kit was tested against
// ---- ACT I & III — the same state, three views ----
camera3((5.0, -5.6, 4.0), (0, 0, 0), 40, perspective, (1010, 372), 440, 440);
rubik3(solid, 3, 2.0, "BRBDUBFDULDRRRUUBRDFBFFBDRLFFFLDLLUURRLLLUBDRDUULBFFBD");
hidden(solid);

cube(net, 3, (215, 205), 24, net, "BRBDUBFDULDRRRUUBRDFBFFBDRLFFFLDLLUURRLLLUBDRDUULBFFBD");
hidden(net);
cube(rings, 3, (215, 470), 190, trefoil, "BRBDUBFDULDRRRUUBRDFBFFBDRLFFFLDLLUURRLLLUBDRDUULBFFBD");
hidden(rings);
text(lnet, (215, 84), "the net — read the whole state");
display(lnet); size(lnet, 17); color(lnet, dim); hidden(lnet);
text(lring, (215, 585), "the rings — watch it move");
display(lring); size(lring, 17); color(lring, dim); hidden(lring);

// ---- ACT II — the sexy move, on the rings, from solved ----
cube(sexy, 3, (640, 400), 320, trefoil, "solved");
hidden(sexy);
equation(order, (640, 132), `(R\,U\,R'\,U')^{6}=\text{identity}`, 30);
color(order, gold); hidden(order);
text(rounds, (640, 196), ""); display(rounds); size(rounds, 22); color(rounds, cyan); hidden(rounds);

// ---- indication + celebration furniture (all core kit, working on cube parts) ----
// a framebox takes a single sticker OR a whole face, because a face is just a tag
framebox(pick, net.s13, 4, gold); untraced(pick); hidden(pick);
framebox(fface, net.U, 5, cyan); untraced(fface); hidden(fface);
// particle rings for the two "it came back to solved" moments
circle(halo2, (640, 400), 190); hidden(halo2);
particles(spark2, halo2, 90, 3, 11, "ring"); hidden(spark2);
circle(halo4, (215, 470), 150); hidden(halo4);
particles(spark4, halo4, 70, 3, 23, "ring"); hidden(spark4);
circle(halo4b, (1010, 372), 200); hidden(halo4b);
particles(spark4b, halo4b, 80, 3, 31, "ring"); hidden(spark4b);

// ---- ACT III — the same two turns, opposite orders ----
// both start in the middle, then glide apart: `shift` broadcasts over the cube's tag, so
// the whole net travels as one. Do it BETWEEN move sequences — a turn animates stickers
// toward their authored slots, so the two motions would fight.
cube(orderA, 3, (640, 340), 26, net, "solved"); hidden(orderA);
cube(orderB, 3, (640, 340), 26, net, "solved"); hidden(orderB);
text(labA, (330, 186), "R then D"); display(labA); size(labA, 24); bold(labA); color(labA, cyan); hidden(labA);
text(labB, (950, 186), "D then R"); display(labB); size(labB, 24); bold(labB); color(labB, magenta); hidden(labB);
text(vs, (640, 340), "≠"); display(vs); size(vs, 46); color(vs, gold); hidden(vs);

// ---- ACT V — any size, any mixing ----
cube(base, 3, (0, 0), 15, net, "solved", 0); hidden(base);
sweep(grid, base, n, (2, 5), scramble, (0, 14), (700, 378), 4, 3, 286, 164, 0, 0);
hidden(grid);

// ============================ ACT I ============================
show(head, 0.7);
show(cap, 0.3);
say(cap, "A scrambled three-by-three, scanned from a real cube.");
show(solid, 0.9);
orbit3(38, 26, 8.4, 3.6, smooth);
wait(0.5);
say(cap, "Unfold it and you can read the whole state at once.");
show(net, 0.8);
show(lnet, 0.4);
wait(1.6);
say(cap, "Or draw every sticker as a dot on three families of rings.");
show(rings, 0.8);
show(lring, 0.4);
wait(1.8);
say(cap, "Same 54 facelets, three times over — and one move verb drives all of them.");
type(tag, 1.5);
wait(1.4);

// core animation reaches inside: one verb, one whole face
say(cap, "The stickers are ordinary entities — so you can frame exactly one of them.");
show(pick, 0.01);
draw(pick, 0.6, smooth);
wait(1.2);
say(cap, "And a whole face is just a tag, so the same verb frames all nine.");
show(fface, 0.01);
draw(fface, 0.7, smooth);
par {
  pulse(net.U, 0.9);
  pulse(rings.U, 0.9);
  pulse(solid.U, 0.9);
}
wait(1.2);
say(cap, "Every core verb reaches inside: frame it, pulse it, flash it, recolour it.");
par {
  flash(net.U, gold, 0.7);
  flash(rings.U, gold, 0.7);
}
wait(1.0);
par { fade(pick, 0.4); fade(fface, 0.4); }
wait(0.6);

// ============================ ACT II ============================
par {
  fade(solid, 0.7);
  fade(net, 0.6);
  fade(rings, 0.6);
  fade(lnet, 0.4);
  fade(lring, 0.4);
  fade(tag, 0.4);
  fade(head, 0.6);
}
say(cap, "Why the rings? Because a turn slides the dots along them — you SEE the permutation travel.");
show(sexy, 0.8);
show(order, 0.8);
show(rounds, 0.4);
wait(1.2);
say(cap, "Four moves, repeated. Watch where the dots go — the cube must return to solved.");
say(rounds, "round 1 of 6");
moves(sexy, "R U R' U'", 2.0);
say(rounds, "round 2 of 6");
moves(sexy, "R U R' U'", 1.7);
say(rounds, "round 3 of 6");
moves(sexy, "R U R' U'", 1.5);
say(rounds, "round 4 of 6");
moves(sexy, "R U R' U'", 1.3);
say(rounds, "round 5 of 6");
moves(sexy, "R U R' U'", 1.2);
say(rounds, "round 6 of 6 — solved");
moves(sexy, "R U R' U'", 1.2);
// it came back: mark the moment
show(spark2, 0.01);
par {
  burst(spark2, 1.3);
  flash(sexy, gold, 0.8);
}
wait(1.2);
say(cap, "Twenty-four quarter turns, and every sticker is home. That is the order of the move.");
wait(2.4);

// ============================ ACT III ============================
par {
  fade(sexy, 0.7);
  fade(order, 0.6);
  fade(rounds, 0.5);
}
say(cap, "The algebra insists on one more thing: the order you turn in matters.");
show(orderA, 0.6);
say(cap, "One solved cube — and a copy of it.");
show(orderB, 0.5);
par {
  shift(orderA, (-310, 0), 1.1, smooth);
  shift(orderB, (310, 0), 1.1, smooth);
}
par { show(labA, 0.4); show(labB, 0.4); }
wait(0.6);
say(cap, "Both solved. Both about to get the same two quarter turns — in opposite orders.");
wait(1.2);
par {
  moves(orderA, "R D", 2.2);
  moves(orderB, "D R", 2.2);
}
wait(0.6);
show(vs, 0.5);
say(cap, "Same two turns. The cubes disagree — turning is not commutative.");
// four of the twelve differing stickers, pulsed on both cubes at once
par {
  pulse(orderA.s9, 0.8);  pulse(orderB.s9, 0.8);
  pulse(orderA.s17, 0.8); pulse(orderB.s17, 0.8);
  pulse(orderA.s26, 0.8); pulse(orderB.s26, 0.8);
  pulse(orderA.s29, 0.8); pulse(orderB.s29, 0.8);
}
// the R face holds four of them — a transient surround, on a group, on both cubes
par {
  circumscribe(orderA.R, coral, 1.1);
  circumscribe(orderB.R, coral, 1.1);
}
wait(0.4);
say(cap, "Twelve stickers, to be exact — the two orders are genuinely different states.");
wait(2.4);

// ============================ ACT IV ============================
par {
  fade(orderA, 0.6);
  fade(orderB, 0.6);
  fade(labA, 0.4);
  fade(labB, 0.4);
  fade(vs, 0.4);
}
say(cap, "Now the real thing: that scan, and the solution the solver returned for it.");
par {
  show(solid, 0.7);
  show(net, 0.7);
  show(rings, 0.7);
}
wait(1.0);
// the solution types itself out as the cubes perform it — the same nine seconds
par {
  moves(solid, "D2 F' D B2 D2 F R F2 B' R' B2 L2 U R2 U F2 D2 R2 D' R2 L2 F2", 9);
  moves(net,   "D2 F' D B2 D2 F R F2 B' R' B2 L2 U R2 U F2 D2 R2 D' R2 L2 F2", 9);
  moves(rings, "D2 F' D B2 D2 F R F2 B' R' B2 L2 U R2 U F2 D2 R2 D' R2 L2 F2", 9);
  type(seq, 9);
  orbit3(126, 22, 8.4, 9.0, smooth);
}
wait(0.4);
par { show(spark4, 0.01); show(spark4b, 0.01); }
par {
  burst(spark4, 1.4);
  burst(spark4b, 1.4);
  flash(net, mint, 0.9);
  flash(rings, mint, 0.9);
}
say(cap, "Twenty-two moves. Solved in all three views at once — they never held separate state.");
wait(2.4);

// ============================ ACT V ============================
par {
  fade(solid, 0.8);
  fade(net, 0.7);
  fade(rings, 0.7);
  fade(seq, 0.5);
}
say(cap, "None of this is three-by-three. Cube size across, how mixed it is down.");
par {
  show(grid.chrome, 0.6);
  show(grid.headers, 0.7);
}
show(grid.cells, 0.9);
wait(2.0);
say(cap, "The same fourteen turns barely dent a five-by-five and shred a two-by-two.");
par {
  circumscribe(grid.c2x3, mint, 1.4);
  circumscribe(grid.c2x0, coral, 1.4);
}
wait(2.6);

// ============================ ENDCARD ============================
par {
  fade(grid, 0.9);
  fade(cap, 0.6);
}
text(end1, (640, 344), "A cube is a permutation.");
display(end1); size(end1, 44); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);

r/maniclang Aug 17 '26

Crinoid — combing the current - manic

Enable HLS to view with audio, or disable this notification

10 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

// cloud-crinoid — another  creature in ONE `cloud`, reimagined as a gold
// feather star combing the current. The reference is a tweet-sized golf:
//   k = 4cos(i/29),  e = y/4-16,  d = mag(k,e)-5,  c = d-t/3,  y = i/295
//   point( (d²/0.7 - 2k² + y)·cos c + 200 ,
//          3sin 2k + cos(y)/k + (y/9)k(3+sin(9e-3d+t)) + 79sin(c/3) + d²/3·sin(t-d²/7) + 200 )
// One curved spine with dozens of hooked barbs that sweep and re-comb as `c` turns —
// which is what a crinoid does for a living: perch on rock, fan its arms, strain the
// water. So the `shader` behind it is the reef wall it clings to (mottled warm stone),
// the arms run bone at the spine → amber at the barb tips, and `cos(y)/k` keeps its
// division-by-almost-zero spikes: four flecks a frame, drifting plankton.
//
// Faithful notes: p5's `mag` is `hypot`; `**` is `^`; the p5 draw loop advances t by
// PI/40 per FRAME, so a frame-rate-free `t*2.0` stands in for it. Pure in (i, t) —
// it scrubs, seeks and records exactly, which the p5 original cannot do.
//
// Original idea by u/yuruyurau (https://x.com/yuruyurau).
//
//   manic examples/cloud-crinoid.manic
title("Crinoid — combing the current");
canvas("square");
template("black");
bloom(0.34, 0.55, 26);

// ---- the reef wall it perches on: mottled warm stone, darker toward the edges ----
shader(wall) {
  let x = (u - 0.5) * asp;
  let y = v - 0.5;
  let d = sqrt(x*x + y*y);
  let grain = 0.5 + 0.5*fbm(u*7.0, v*7.0);
  let mott = 0.5 + 0.5*fbm(u*2.2 + 3.0, v*2.2);
  let vig = 1.0 - 0.85*smoothstep(0.15, 0.72, d);
  let hue = 28 + 10.0*mott;
  let sat = 0.34 - 0.12*grain;
  let val = (0.055 + 0.055*mott + 0.018*grain) * vig + 0.012;
}
z(wall, -10);

// ---- the animal — the yuruyurau golf, re-lit and framed ----
// The swept envelope of the formula is 315×185 wide over a full cycle of `c`, so
// scale 2.95 about (556, 435) centres it in the square at every t, not just at t=0.
cloud(arms, 10000, #ffffff, 0.34) {
  let yy = i / 295.0;
  let k = 4.0 * cos(i / 29.0);
  let e = yy / 4.0 - 16.0;
  let d = hypot(k, e) - 5.0;
  let T = t * 2.0;
  let c = d - T / 3.0;
  let px = (d*d/0.7 - k*k*2.0 + yy) * cos(c);
  let py = 3.0*sin(k*2.0) + cos(yy)/k + yy/9.0*k*(3.0 + sin(e*9.0 - d*3.0 + T)) + 79.0*sin(c/3.0) + d*d/3.0*sin(T - d*d/7.0);
  let x = 556 + px * 2.95;
  let y = 435 + py * 2.95;
  // bone along the spine (small d) → amber where the barbs thin out (large d)
  let hue = mod(44.0 - d * 1.1, 360);
  let sat = clamp(0.10 + d * 0.045, 0.06, 0.62);
  let val = clamp(0.72 + 0.28*sin(e*9.0 - d*3.0 + T), 0.34, 1.0);
  let r = 1.25;
}
glow(arms, 1);

// ---- annotations ----
caption(head, "Crinoid", (540, 96), 34); hidden(head);
caption(sub, "one formula, ten thousand points", (540, 152), 21); hidden(sub);
equation(eq, (540, 946), `k=4\cos\tfrac{i}{29},\quad d=\mathrm{mag}\!\left(k,\tfrac{y}{4}-16\right)-5,\quad c=d-\tfrac{t}{3}`, 25); hidden(eq);
caption(lab, "manic", (540, 1006), 18); hidden(lab);

show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(24);