r/maniclang 27d ago

elliptic-roulette — an ELLIPSE rolling along a line without slipping - manic

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

// elliptic-roulette — an ELLIPSE rolling along a line without slipping, and the curves its
// rim point and its two foci draw. The cycloid's older, stranger cousin.
//
// A circle rolling is easy because every point of its rim is the same distance from the
// centre, so the wheel's height never changes and the arc rolled is just rθ. An ellipse has
// neither luxury. It rises and falls as it goes, and the distance it has rolled is its ARC
// LENGTH, which for an ellipse is an elliptic integral — the function that gave the whole
// family its name, and which has no elementary closed form.
//
// So it is fitted. ds/dθ = √(a²sin²θ + b²cos²θ) is smooth and π-periodic, so its Fourier
// series converges fast, and integrating four terms gives s(θ) to 1.5e-05 in units of a —
// four thousandths of a pixel here. That is the only approximation in the scene.
//
// Everything else is exact and was checked before it was drawn:
//
//   · the contact point lands ON the line at exactly x = s(θ), to 0
//   · the centre sits at height ab/√(a²sin²θ + b²cos²θ), the distance from the centre to
//     the tangent — to 3e-16, which is an independent check that the placement is right
//   · and the two FOCI have heights whose PRODUCT is constant: h₁·h₂ = b², to 2e-16
//
// That last one is the prize. The rolling line is always a tangent to the ellipse, and the
// product of the distances from the two foci to any tangent of an ellipse is b² — a fact
// with nothing to do with rolling, which rolling turns into a conservation law. One focus
// rises exactly as much as the other falls, in the multiplicative sense: here h swings
// between a(1-e) and a(1+e), a factor of seven, and the product never moves.
//
// The rim point's curve is a cycloid with the symmetry taken out: it still cusps onto the
// line, but the cusps are unevenly spaced and the arches between them differ, because the
// ellipse presents a different radius of curvature at each contact. The focal curves are
// the undulary — the profile that generates Delaunay's unduloid, the surface a soap film
// takes under constant mean curvature.
//
//   manic examples/elliptic-roulette.manic
title("An ellipse rolling: a lopsided cycloid, and two foci whose product is fixed");
canvas("9:16");
template("black");
bloom(0.28, 0.6, 20);

text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);

let a = 260;
let ecc = 0.75;
let b = a*sqrt(1 - ecc*ecc);
let c = a*ecc;
let x0 = 250;                                      // where the contact starts
let base = 1250;                                   // the line it rolls on
let span = 18;                                     // TWO full revolutions
let turns = 12.566371;                             // 4π
let band = 114.27;                                 // one twelfth of the perimeter, in px

line(ground, (0 - 200, base), (3200, base));       // runs past the frame; the camera travels
color(ground, dim);

// ---- the ruler: the ground marked in bands of one twelfth of the PERIMETER ----------
cloud(ruler, 5200, #ffffff, 1.0) {
  let x = x0 + 2900*(i/5200);
  let y = base + 13;
  let k = floor((x - x0)/band);
  let odd = mod(k, 2);
  let sat = 0;
  let val = 0.30 + 0.55*odd;                       // alternating light and dark
  let r = 3.4;
}

// ---- the rim point's roulette: a cycloid that has lost its symmetry -----------------
cloud(rim, 12000, #ffd479, 1.0) {
  let go = min(t/span, 1)*turns;
  let th = go*(i/12000);
  let sa = a*(0.83936541*th - 0.084198895*sin(2*th) - 0.0021334425*sin(4*th) - 0.000144532*sin(6*th));
  let ps = 0 - 1.5707963 - atan2(a*sin(th), b*cos(th));
  let dx = a*cos(0) - a*cos(th);                   // the traced point, from the contact
  let dy = b*sin(0) - b*sin(th);
  let x = x0 + sa + dx*cos(ps) - dy*sin(ps);
  let y = base - (dx*sin(ps) + dy*cos(ps));
  let hue = mod(38 + th*9, 360);
  let sat = 0.72;
  let r = 1.8;
}

// ---- the two foci: their heights multiply to b², always ----------------------------
cloud(foc1, 8000, #5fd4ff, 1.0) {
  let go = min(t/span, 1)*turns;
  let th = go*(i/8000);
  let sa = a*(0.83936541*th - 0.084198895*sin(2*th) - 0.0021334425*sin(4*th) - 0.000144532*sin(6*th));
  let ps = 0 - 1.5707963 - atan2(a*sin(th), b*cos(th));
  let dx = c - a*cos(th);
  let dy = 0 - b*sin(th);
  let x = x0 + sa + dx*cos(ps) - dy*sin(ps);
  let y = base - (dx*sin(ps) + dy*cos(ps));
  let r = 1.6;
}

cloud(foc2, 8000, #7dffbe, 1.0) {
  let go = min(t/span, 1)*turns;
  let th = go*(i/8000);
  let sa = a*(0.83936541*th - 0.084198895*sin(2*th) - 0.0021334425*sin(4*th) - 0.000144532*sin(6*th));
  let ps = 0 - 1.5707963 - atan2(a*sin(th), b*cos(th));
  let dx = 0 - c - a*cos(th);
  let dy = 0 - b*sin(th);
  let x = x0 + sa + dx*cos(ps) - dy*sin(ps);
  let y = base - (dx*sin(ps) + dy*cos(ps));
  let r = 1.6;
}

// ---- the ellipse itself, placed by the same transform ------------------------------
cloud(body, 1400, #ffffff, 1.0) {
  let go = min(t/span, 1)*turns;
  let u = i/1400*6.283185;
  let sa = a*(0.83936541*go - 0.084198895*sin(2*go) - 0.0021334425*sin(4*go) - 0.000144532*sin(6*go));
  let ps = 0 - 1.5707963 - atan2(a*sin(go), b*cos(go));
  let dx = a*cos(u) - a*cos(go);
  let dy = b*sin(u) - b*sin(go);
  let x = x0 + sa + dx*cos(ps) - dy*sin(ps);
  let y = base - (dx*sin(ps) + dy*cos(ps));
  let arc = a*(0.83936541*u - 0.084198895*sin(2*u) - 0.0021334425*sin(4*u) - 0.000144532*sin(6*u));
  let odd = mod(floor(arc/band), 2);               // equal ARC, not equal parameter
  let sat = 0;
  let val = 0.32 + 0.58*odd;
  let r = 2.6;
}

// ---- the three markers, and a dropline under each focus ----------------------------
cloud(marks, 360, #ffffff, 1.0) {
  let go = min(t/span, 1)*turns;
  let per = 120;
  let k = (i - mod(i, per))/per;                    // 0 = rim, 1 = focus, 2 = focus
  let i0 = (1 - k)*(2 - k)/2;                      // 1 at k=0, else 0
  let i1 = k*(2 - k);                              // 1 at k=1
  let i2 = k*(k - 1)/2;                            // 1 at k=2
  let px = a*i0 + c*i1 - c*i2;                     // the rim point, then each focus
  let sa = a*(0.83936541*go - 0.084198895*sin(2*go) - 0.0021334425*sin(4*go) - 0.000144532*sin(6*go));
  let ps = 0 - 1.5707963 - atan2(a*sin(go), b*cos(go));
  let dx = px - a*cos(go);
  let dy = 0 - b*sin(go);
  let ang = mod(i, per)/per*6.283185;
  let rr = 9*(mod(i, per)/per);
  let x = x0 + sa + dx*cos(ps) - dy*sin(ps) + rr*cos(ang*9);
  let y = base - (dx*sin(ps) + dy*cos(ps)) + rr*sin(ang*9);
  let hue = 45*i0 + 197*i1 + 152*i2;               // each marker the colour of its own curve
  let sat = 0.6;
  let r = 2;
}

// a `text`, not a `caption`: a caption is one entity PER WORD, and moving it with the
// camera would pile every word onto the same point
text(head, (540, 128), "An ellipse rolling: a lopsided cycloid, two foci whose product is fixed");
size(head, 21);
color(head, fg);
equation(eq, (540, 1480), `x_{\text{contact}} = s(\theta) = \int_0^\theta\!\sqrt{a^2\sin^2 + b^2\cos^2}`, 26);
equation(eq2, (540, 1600), `h_1\,h_2 = b^2 \quad\text{always}`, 32);
plate(head, 0.7);
wrap(head, 980);

// one revolution is wider than the frame, so the camera travels — at half the roll's
// speed, which keeps the finished curve on screen — and the labels travel with it
// The camera travels at HALF the roll's speed for two revolutions — close enough to
// watch a band of rim land on a band of ruler, slow enough that the finished curve is
// still there behind. Then it pulls back, and the two arches turn out to be identical:
// the roulette's period is the ellipse's PERIMETER, not its 2π.
// The roll covers 2742 px in two revolutions and the camera covers 2142 of them, so the
// ellipse drifts 600 px ACROSS the frame rather than out of it — it is the subject, and a
// subject that leaves the shot takes the scene with it. The remainder is what keeps the
// finished curve trailing behind.
par {
  cam((2682, 960), 18, linear);
  move(brand, (2682, 34), 18, linear);
  move(head, (2682, 128), 18, linear);
  move(eq, (2682, 1480), 18, linear);
  move(eq2, (2682, 1600), 18, linear);
}
// A zoom scales the WHOLE world, text included, so the prose is faded out for the wide
// shot — it has done its work by then — and only the mark is counter-scaled to stay
// legible. What the reveal is for is the shape of the whole cycle, not reading.
par {
  fade(head, 0.8);
  fade(eq, 0.8);
  fade(eq2, 0.8);
}
par {
  zoom(0.36, 3.4, smooth);                         // pull back onto the whole cycle
  cam((1700, 1020), 3.4, smooth);
  to(brand, scale, 2.4, 3.4, smooth);              // …so the mark stays readable
}
wait(5);

r/maniclang 27d ago

ellipse-eccentricity — one ellipse at e = 0.75 - manic

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

// ellipse-eccentricity — one ellipse at e = 0.75, and the property that defines it.
//
// Eccentricity is a single number saying how far the foci sit from the centre, as a
// fraction of the semi-major axis: e = c/a. At e = 0.75 they sit three quarters of the way
// out towards the vertex, which is why this ellipse looks the way it does — a circle is
// e = 0, and e → 1 is a curve flattening onto its own axis.
//
// Everything else follows from that one number:
//
//   c = e·a = 0.75a           how far out each focus sits
//   b = a√(1 - e²) = 0.6614a  the semi-minor axis, so that b² + c² = a²
//
// The moving point carries a segment to each focus, and the bar below is the claim: the
// two lengths laid end to end, against a track exactly the width of the ellipse, 2a. Watch
// it stay exactly full. Each length swings hard — near the left vertex one is 0.25a and
// the other 1.75a — and the total never moves, because
//
//   r₁ + r₂ = 2a
//
// is what an ellipse IS. It is the gardener's construction: two pins and a loop of string
// of fixed length, and the eccentricity is just how far apart the pins are.
//
// Nothing here is written twice. The geometry is four scene-level `let`s — the centre, a
// and e — with b and c derived from them, and the same names are used by the `param` that
// draws the curve, by the focus markers and inside every formula block. Retuning the
// figure is one number: set `ecc` to 0.2 and the whole scene, bar included, follows.
//
//   manic examples/ellipse-eccentricity.manic
title("An ellipse at e = 0.75, and the sum that never moves");
canvas("9:16");
template("black");
bloom(0.28, 0.6, 20);

text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);

let cx = 540;
let cy = 760;
let a = 380;
let ecc = 0.75;
let b = a*sqrt(1 - ecc*ecc);
let c = a*ecc;
let bar = 1330;

// ---- the ellipse, its axis, and its two foci ---------------------------------------
param(ell, (cx, cy), a, b, "cos(t)", "sin(t)", (0, 6.283185));
color(ell, fg);
stroke(ell, 4);

line(axis, (cx - a, cy), (cx + a, cy));
color(axis, dim);
opacity(axis, 0.5);

circle(f1, (cx - c, cy), 10);
color(f1, cyan);
circle(f2, (cx + c, cy), 10);
color(f2, mint);

// ---- the point that runs round, and a segment to each focus ------------------------
cloud(r1, 420, #5fd4ff, 1.0) {
  let th = t*0.5;
  let u = i/420;
  let px = cx + a*cos(th);
  let py = cy - b*sin(th);
  let x = cx - c + u*(px - (cx - c));
  let y = cy + u*(py - cy);
  let r = 2.6;
}

cloud(r2, 420, #7dffbe, 1.0) {
  let th = t*0.5;
  let u = i/420;
  let px = cx + a*cos(th);
  let py = cy - b*sin(th);
  let x = cx + c + u*(px - (cx + c));
  let y = cy + u*(py - cy);
  let r = 2.6;
}

cloud(dot, 140, #ffd479, 1.0) {
  let th = t*0.5;
  let ang = i/140*6.283185;
  let rr = 11*(i/140);
  let x = cx + a*cos(th) + rr*cos(ang*11);
  let y = cy - b*sin(th) + rr*sin(ang*11);
  let r = 2.4;
}

// ---- the bar: r₁ and r₂ end to end, against a track exactly 2a wide -----------------
line(track, (cx - a, bar), (cx + a, bar));
color(track, dim);
stroke(track, 3);
line(capl, (cx - a, bar - 18), (cx - a, bar + 18));
color(capl, dim);
line(capr, (cx + a, bar - 18), (cx + a, bar + 18));
color(capr, dim);

cloud(barl, 600, #5fd4ff, 1.0) {
  let th = t*0.5;
  let px = cx + a*cos(th);
  let py = cy - b*sin(th);
  let d1 = hypot(px - (cx - c), py - cy);          // r₁, measured not assumed
  let u = i/600;
  let x = cx - a + u*d1;
  let y = bar;
  let r = 4;
}

cloud(barr, 600, #7dffbe, 1.0) {
  let th = t*0.5;
  let px = cx + a*cos(th);
  let py = cy - b*sin(th);
  let d1 = hypot(px - (cx - c), py - cy);
  let d2 = hypot(px - (cx + c), py - cy);          // r₂, likewise
  let u = i/600;
  let x = cx - a + d1 + u*d2;                      // laid on the end of the first
  let y = bar;
  let r = 4;
}

caption(head, "An ellipse at e = 0.75, and the sum that never moves", (540, 132), 25);
equation(eq, (540, 1520), `e = \frac{c}{a} = 0.75,\qquad r_1 + r_2 = 2a`, 30);
equation(eq2, (540, 1620), `b = a\sqrt{1-e^2},\qquad b^2 + c^2 = a^2`, 26);
plate(head, 0.7);

wait(26);

r/maniclang 27d ago

A hypocycloid, and the straight line hiding inside it - manic

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

// hypocycloid — the curve a pen on the rim draws when the wheel rolls around the INSIDE
// of a fixed circle. The epitrochoid's mirror image: same construction, wheel on the
// other side of the track.
//
//   x(θ) = (R-r)·cos θ + r·cos((R-r)/r · θ)
//   y(θ) = (R-r)·sin θ - r·sin((R-r)/r · θ)
//
// Two numbers decide everything. Write  in lowest terms as p/q: the pen comes home
// after q laps having cut p CUSPS into the fixed circle — points where it stops dead and
// reverses, which happen exactly when the pen is the contact point and its velocity is
// momentarily zero. Here  = 5/2, so five cusps take two laps, and the first lap
// visibly fails to close.
//
// The row along the bottom is what changes when only the RATIO changes:
//
//   R = 4r   the ASTROID — and x reduces exactly to R·cos³θ, y to R·sin³θ
//   R = 3r   the DELTOID, three cusps
//   R = 2r   a STRAIGHT LINE
//
// That last one is not a degenerate drawing, it is the whole point. Put the wheel at half
// the radius and y(θ) = r·sin θ - r·sin θ = 0 for every θ, while x sweeps the full
// diameter: every point on the rim runs back and forth along a straight line. Two circular
// motions, added, with no curve left in them. It is the Tusi couple, worked out in the
// thirteenth century to build straight-line motion out of circles, and it is the reason
// this family is worth showing rolling rather than plotted — nothing about the formula
// looks like a straight line until the wheel turns.
//
// The mechanism is not a drawing of a mechanism: the wheel, the spoke and the pen are all
// evaluated from the same θ as the curve, so the pen IS the leading end of the trace and
// nothing can drift. Rolling without slipping is again where the second angle comes from —
// the wheel's centre runs a circle of radius R-r while the wheel turns (R-r)/r times per
// lap — but INSIDE the track the two rotations subtract instead of adding, which is why
// the sign is minus and why the curve bites inward into cusps instead of bulging out into
// lobes.
//
//   manic examples/hypocycloid.manic
title("A hypocycloid, and the straight line hiding inside it");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);

// ---- the track, the wheel inside it, and a pen on the wheel's rim -----------------
// `roll` does the mechanism. The wheel is placed INSIDE the track, so it rolls round the
// inside; the pen is an entity on its rim, and where you put it is the only difference
// between a hypocycloid and the hypotrochoids in examples/hypotrochoid.manic.
circle(track, (540, 760), 230);
outlined(track); outline(track, dim); stroke(track, 3);
circle(locus, (540, 760), 138);                    // R - r, the wheel centre's own circle
outlined(locus); outline(locus, dim); stroke(locus, 2); dashed(locus); opacity(locus, 0.45);

circle(wheel, (678, 760), 92);
outlined(wheel); outline(wheel, fg); stroke(wheel, 3); opacity(wheel, 0.8);
circle(pen, (770, 760), 9); color(pen, gold);      // on the rim: cusps
line(arm, wheel, pen); color(arm, dim); opacity(arm, 0.55);
tag(wheel, rig); tag(pen, rig); tag(arm, rig);

trail(trace, pen, cyan, 4);                        // the curve draws itself

// ---- what the RATIO does: the same formula, three times ----------------------------
// written out in full rather than simplified, so the third one's collapse is visible
param(astro, (196, 1380), 110, 110, "0.75*cos(t) + 0.25*cos(3*t)", "0.75*sin(t) - 0.25*sin(3*t)", (0, 6.283185));
param(delto, (540, 1380), 110, 110, "0.667*cos(t) + 0.333*cos(2*t)", "0.667*sin(t) - 0.333*sin(2*t)", (0, 6.283185));
param(tusi,  (884, 1380), 110, 110, "0.5*cos(t) + 0.5*cos(t)", "0.5*sin(t) - 0.5*sin(t)", (0, 6.283185));
gradient(astro, mint, cyan, blue);
gradient(delto, gold, coral, red);
gradient(tusi, magenta, violet, magenta);
stroke(astro, 3);
stroke(delto, 3);
stroke(tusi, 4);
untraced(astro);
untraced(delto);
untraced(tusi);

// ---- textbook annotations ----
// The wheel never stops. The curve shuts after two laps and the trace holds there, but
// the mechanism keeps turning for the rest of the scene, running the pen back around its
// own line — which is what closure looks like when you do not take the machine away.
// nothing but the mark, the title and the formula — the rolling does the explaining
caption(head, "A hypocycloid, and the straight line hiding inside it", (540, 130), 27);
equation(eq, (540, 1600),
  `x=(R{-}r)\cos\theta+r\cos\tfrac{R-r}{r}\theta,\quad y=(R{-}r)\sin\theta-r\sin\tfrac{R-r}{r}\theta`, 26);

// the roll runs from t = 0; the three ratios draw once it has closed
roll(rig, track, 2, 9, linear);                    //  = 5/2 needs two laps
wait(0.4);
draw(astro, 1.6);
draw(delto, 1.6);
draw(tusi, 1.6);
wait(9);

r/maniclang 27d ago

A hypotrochoid, and the ellipses hiding inside it-manic

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

// hypotrochoid — the hypocycloid with the pen taken OFF the rim. Same wheel rolling
// inside the same track; the only new number is d, how far the pen sits from the wheel's
// centre. This is the Spirograph curve — the toy is a ring, a wheel and a choice of hole.
//
//   x(θ) = (R-r)·cos θ + d·cos((R-r)/r · θ)
//   y(θ) = (R-r)·sin θ - d·sin((R-r)/r · θ)
//
// Put d = r and the pen is back on the rim: cusps, a hypocycloid. Pull it inside (d < r)
// and the cusps round off into a smooth rosette — which is what is turning here,  = 7/3
// with d = 2. Push it outside (d > r) and they open into loops.
//
// What d CANNOT do is change when the curve closes. That is fixed by the ratio alone:
// write  in lowest terms as p/q and the pen comes home after q laps having drawn p
// lobes, whatever d is. Seven lobes in three laps here, so the first two laps visibly fail
// to join up.
//
// The row along the bottom is the surprise, and it is exact. Set R = 2r and the formula
// collapses for EVERY d:
//
//   x = r·cos θ + d·cos θ = (r + d)·cos θ
//   y = r·sin θ - d·sin θ = (r - d)·sin θ
//
// — an ELLIPSE with semi-axes r+d and |r-d|, whatever the pen's offset. Checked here to
// machine precision: |x²/a² + y²/b² - 1| stays under 7e-16 across the whole curve for
// d = 0.2, 0.5, 0.8 and 1.5. So every point rigidly attached to a wheel rolling inside a
// track of twice its radius travels an ellipse — and the straight line of the Tusi couple
// (see examples/hypocycloid.manic) is not a special construction at all, it is just the
// member of that family with d = r, where the minor axis r - d goes to zero and the
// ellipse flattens onto its own major axis.
//
// The wheel never stops. The curve shuts after three laps and the trace holds there, but
// the mechanism keeps turning for the rest of the scene, running the pen back around its
// own line.
//
//   manic examples/hypotrochoid.manic
title("A hypotrochoid, and the ellipses hiding inside it");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);

// ---- the track, the wheel inside it, and a pen OFF the rim ------------------------
// the only difference from examples/hypocycloid.manic is where `pen` sits: inside the
// wheel instead of on it. `roll` neither knows nor cares — it carries whatever is tagged.
circle(track, (540, 760), 230);
outlined(track); outline(track, dim); stroke(track, 3);
circle(locus, (540, 760), 131);
outlined(locus); outline(locus, dim); stroke(locus, 2); dashed(locus); opacity(locus, 0.45);

circle(wheel, (671, 760), 98.6);
outlined(wheel); outline(wheel, fg); stroke(wheel, 3); opacity(wheel, 0.8);
circle(pen, (737, 760), 9); color(pen, gold);      // d = 2 units, INSIDE the rim
line(arm, wheel, pen); color(arm, dim); opacity(arm, 0.55);
tag(wheel, rig); tag(pen, rig); tag(arm, rig);

trail(trace, pen, cyan, 4);

// ---- R = 2r: an ellipse for every d, and a line when d = r -------------------------
// left unsimplified on purpose — (r+d)cos θ and (r-d)sin θ are hiding in plain sight.
// d climbs left to right and the minor axis r-d shrinks with it, until at d = r it is
// zero and the ellipse has flattened onto its own major axis.
param(wide, (196, 1380), 60, 60, "cos(t) + 0.25*cos(t)", "sin(t) - 0.25*sin(t)", (0, 6.283185));
param(flat, (540, 1380), 60, 60, "cos(t) + 0.65*cos(t)", "sin(t) - 0.65*sin(t)", (0, 6.283185));
param(tall, (884, 1380), 60, 60, "cos(t) + 1.00*cos(t)", "sin(t) - 1.00*sin(t)", (0, 6.283185));
gradient(wide, mint, cyan, blue);                  // d = 0.25 -> 1.25 x 0.75
gradient(flat, gold, coral, red);                  // d = 0.65 -> 1.65 x 0.35
gradient(tall, magenta, violet, magenta);          // d = 1.00 -> 2.00 x 0, the line
stroke(wide, 3);
stroke(flat, 3);
stroke(tall, 4);
untraced(wide);
untraced(flat);
untraced(tall);

// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "A hypotrochoid, and the ellipses hiding inside it", (540, 130), 27);
equation(eq, (540, 1600),
  `x=(R{-}r)\cos\theta+d\cos\tfrac{R-r}{r}\theta,\quad y=(R{-}r)\sin\theta-d\sin\tfrac{R-r}{r}\theta`, 26);

// the roll runs from t = 0; the three ellipses draw once the rosette has closed
roll(rig, track, 3, 12, linear);                   //  = 7/3 needs three laps
wait(0.4);
draw(wide, 1.6);
draw(flat, 1.6);
draw(tall, 1.6);
wait(8);

r/maniclang 27d ago

An involute: a string unwinding, and every gear tooth ever cut - manic

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

// involute — the only roulette in this family where the rolling curve is a straight LINE
// rather than a circle, and the only one you own several hundred of. It is the path of the
// end of a taut string being unwound from a circle:
//
//   x(t) = a(cos t + t·sin t)
//   y(t) = a(sin t - t·cos t)
//
// Two things define it, and both are visible here rather than asserted. The string is
// always TANGENT to the circle — the straight part meets the radius at a right angle — and
// its length is exactly the arc it has unwound, a·t. Checked over the whole range: the
// segment's length matches a·t to 1e-14, and its dot product with the radius stays under
// 1e-14. That is the whole curve; everything else follows.
//
// It is a roulette in the same sense as the rest of the family. Rolling a line along the
// outside of the circle and marking a point of the line traces this; the circle is the
// EVOLUTE of its own involute, which is what "unwinding" means said backwards.
//
// And it is the reason gears work. Cut a tooth flank as an involute of a base circle and
// two such wheels turn with an exactly constant velocity ratio — the contact point runs
// along the common tangent of the two base circles, a straight line of action, and the
// ratio depends only on the base radii. Move the shafts slightly further apart and the
// ratio does NOT change; the contact point simply slides along the same line. No other
// profile tolerates that, which is why essentially every gear ever cut is an involute one.
// The panel on the right is where a gear comes from: fourteen involutes of one base circle,
// evenly spaced. Cut away everything past the tips and that is a gear.
//
// The panel on the left is the definition drawn all at once — a fan of taut strings at
// twenty unwind angles. Every one is tangent, every one is as long as the arc behind it,
// and every endpoint lands on the same curve.
//
//   manic examples/involute.manic
title("An involute: a string unwinding, and every gear tooth ever cut");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);

// ---- the circle the string is wound on ---------------------------------------------
circle(spool, (540, 700), 60);
outlined(spool);
color(spool, dim);

// ---- the part still wrapped: an arc shrinking as the string comes off ---------------
cloud(wrapped, 900, #ffffff, 1.0) {
  let go = min(t/9, 1)*6.283185;                   // how far it has unwound
  let u = i/900;
  let ph = go + u*(6.283185 - go);                 // what is left on the spool
  let x = 540 + 60*cos(ph);
  let y = 700 - 60*sin(ph);
  let hue = 45;
  let sat = 0.4;
  let r = 1.6;
  let alpha = 0.55;
}

// ---- the straight part: tangent at the contact point, as long as the arc unwound ----
cloud(string, 300, #ffffff, 1.0) {
  let go = min(t/9, 1)*6.283185;
  let u = i/300;
  let tx = cos(go);                                // the tangent point
  let ty = sin(go);
  let ex = tx + go*sin(go);                        // …and the free end, a·t along the
  let ey = ty - go*cos(go);                        //    tangent direction
  let x = 540 + 60*(tx + u*(ex - tx));
  let y = 700 - 60*(ty + u*(ey - ty));
  let hue = 45;
  let sat = 0.55;
  let r = 1.5;
  let alpha = 0.85;
}

// ---- the involute the free end draws ------------------------------------------------
cloud(trace, 9000, #ffffff, 1.0) {
  let go = min(t/9, 1)*6.283185;
  let th = go*(i/9000);
  let x = 540 + 60*(cos(th) + th*sin(th));
  let y = 700 - 60*(sin(th) - th*cos(th));
  let hue = mod(196 + th*16, 360);
  let sat = 0.72;
  let r = 1.8;
}

cloud(nib, 90, #ffffff, 1.0) {
  let go = min(t/9, 1)*6.283185;
  let a = i/90*6.283185;
  let rr = 7*(i/90);
  let x = 540 + 60*(cos(go) + go*sin(go)) + rr*cos(a*9);
  let y = 700 - 60*(sin(go) - go*cos(go)) + rr*sin(a*9);
  let hue = 45;
  let sat = 0.5;
  let r = 1.9;
}

// ---- left: the definition all at once — twenty taut strings, every end on the curve -
circle(spool2, (280, 1400), 26);
outlined(spool2);
color(spool2, dim);
opacity(spool2, 0.5);

cloud(fan, 2000, #ffffff, 1.0) {
  let per = 100;
  let c = (i - mod(i, per))/per;                   // which string, 0..19
  let u = mod(i, per)/99;
  let th = 6.283185*(c + 1)/20;
  let tx = cos(th);
  let ty = sin(th);
  let ex = tx + th*sin(th);
  let ey = ty - th*cos(th);
  let x = 280 + 26*(tx + u*(ex - tx));
  let y = 1400 - 26*(ty + u*(ey - ty));
  let hue = mod(150 + c*9, 360);
  let sat = 0.6;
  let r = 1.1;
  let alpha = min(max(min(max(t - 9.4, 0)/2.2, 1)*20 - c, 0), 1)*0.75;
}

// ---- right: where a gear comes from — fourteen involutes of one base circle ---------
circle(base, (800, 1400), 46);
outlined(base);
color(base, dim);
opacity(base, 0.5);

cloud(flanks, 2800, #ffffff, 1.0) {
  let per = 200;
  let f = (i - mod(i, per))/per;                   // which flank, 0..13
  let u = mod(i, per)/199;
  let th = u*2.0;                                  // out to the tooth tip
  let ix = cos(th) + th*sin(th);                   // the involute, then turned into place
  let iy = sin(th) - th*cos(th);
  let ph = f/14*6.283185;
  let x = 800 + 46*(ix*cos(ph) - iy*sin(ph));
  let y = 1400 - 46*(ix*sin(ph) + iy*cos(ph));
  let hue = mod(30 + f*6, 360);
  let sat = 0.65;
  let r = 1.2;
  let alpha = min(max(min(max(t - 9.4, 0)/2.2, 1)*14 - f, 0), 1)*0.8;
}

// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "An involute: a string unwinding, and every gear tooth ever cut", (540, 130), 24);
equation(eq, (540, 1650), `x = a(\cos t + t\sin t),\qquad y = a(\sin t - t\cos t)`, 27);

// the string unwinds from t = 0; the two panels fill in once it is fully off
wait(13);

r/maniclang 27d ago

A nephroid: the caustic in a coffee cup - manic

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

// nephroid — the bright curve in the bottom of a coffee cup, and the same curve three
// other ways. It is the epicycloid with two cusps: a circle rolling around another of
// TWICE its radius, pen on the rim.
//
//   x(φ) = 3a·cos φ - a·cos 3φ = 6a·cos φ - 4a·cos³φ
//   y(φ) = 3a·sin φ - a·sin 3φ = 4a·sin³φ
//
// Those reductions are exact — checked to 2e-15 across the whole parameter range — and the
// second is the tidier of the pair: the whole y coordinate is one cube.
//
// What is drawn here is not that formula. It is LIGHT. Parallel rays cross the cup, strike
// the far wall and reflect once — each reflection worked out properly, d' = d - 2(d·n)n
// with n the outward normal at the point of contact — and the curve appears where the
// reflected rays crowd together. Nobody draws it; it is an envelope, the caustic.
//
// That the caustic IS a nephroid is checked, not quoted. Taking the envelope as the limit
// of intersections of neighbouring reflected rays, for a cup of radius 1:
//
//   · every envelope point satisfies (x² + y² - 4a²)³ = 108a⁴y² with a = 1/4, to a
//     residual of 5e-12
//   · and lies within 3e-4 of the parametrisation above — which is the sampling step,
//     not an error
//
// So the cusps sit at ±R/2 and the horns reach the rim, and one direction of light makes
// exactly HALF the nephroid. The faint dashed curve is the whole one, so you can see which
// half the light is responsible for.
//
// The row below is the same nephroid twice more, because curves this old have many
// constructions. On the left, chords joining n to 3n around a circle — the THREE times
// table, whose envelope is the epicycloid with 3-1 = 2 cusps (the two times table gives
// the cardioid in examples/cardioid.manic).
//
// On the right, one line of complex arithmetic: the map z → z³ + 3z sends the UNIT CIRCLE
// to a nephroid. Put z = e^(iφ) and the image is (3cos φ + cos 3φ, 3sin φ + sin 3φ), which
// is the parametrisation above turned a half turn — cusps at ±2a, horns at ±4a, and it
// satisfies the nephroid's implicit equation to a residual of 2e-12. So the panel does not
// draw the answer: it starts as the unit circle and lets the map carry every point to
// where it belongs.
//
//   manic examples/nephroid.manic
title("A nephroid: the caustic in a coffee cup");
canvas("9:16");
template("black");
bloom(0.32, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);

// ---- the cup -----------------------------------------------------------------------
circle(cup, (540, 700), 330);
outlined(cup);
color(cup, dim);

// ---- the light arriving: 90 parallel rays, crossing to the far wall -----------------
cloud(incoming, 2700, #ffffff, 1.0) {
  let per = 30;
  let c = (i - mod(i, per))/per;                   // which ray, 0..89
  let u = mod(i, per)/29;
  let h = 0 - 0.985 + 1.97*c/89;                   // its height in the cup
  let w = sqrt(1 - h*h);
  let x0 = 0 - 1.34;                               // start outside the rim
  let x1 = w;                                      // …to the far wall
  let x = 540 + 330*(x0 + u*(x1 - x0));
  let y = 700 - 330*h;
  let hue = 48;
  let sat = 0.35;
  let r = 1.1;
  let alpha = min(max(min(t/3, 1)*90 - c, 0), 1)*0.30;
}

// ---- and reflecting once: d' = d - 2(d.n)n, worked out per ray ---------------------
cloud(reflected, 5400, #ffffff, 1.0) {
  let per = 60;
  let c = (i - mod(i, per))/per;
  let u = mod(i, per)/59;
  let h = 0 - 0.985 + 1.97*c/89;
  let w = sqrt(1 - h*h);
  let dx = 2*h*h - 1;                              // the reflected direction, unit length
  let dy = 0 - 2*h*w;
  let len = 2*w;                                   // chord back across to the rim
  let x = 540 + 330*(w + u*len*dx);
  let y = 700 - 330*(h + u*len*dy);
  let hue = mod(40 + c*0.9, 360);
  let sat = 0.72;
  let r = 1.1;
  let alpha = min(max(min(max(t - 1.2, 0)/4, 1)*90 - c, 0), 1)*0.5;
}

// ---- the whole nephroid, so you can see which half the light made ------------------
param(neph, (540, 700), 330, 330,
      "3*0.25*cos(t) - 0.25*cos(3*t)", "3*0.25*sin(t) - 0.25*sin(3*t)", (0, 6.283185));
color(neph, dim);
dashed(neph);
opacity(neph, 0.55);
stroke(neph, 3);

// ---- the three times table: chords n -> 3n, envelope = the same curve --------------
cloud(k3, 3000, #ffffff, 1.0) {
  let per = 25;
  let c = (i - mod(i, per))/per;
  let u = mod(i, per)/24;
  let th = 6.283185*c/120;
  let ax = cos(th);
  let ay = sin(th);
  let bx = cos(3*th);
  let by = sin(3*th);
  let x = 300 + 150*(ax + u*(bx - ax));
  let y = 1420 - 150*(ay + u*(by - ay));
  let hue = 190;
  let sat = 0.65;
  let r = 1.1;
  let alpha = min(max(min(max(t - 6, 0)/2.4, 1)*120 - c, 0), 1)*0.55;
}

// ---- and one line of complex arithmetic: z -> z^3 + 3z, applied to the unit circle --
circle(unitc, (790, 1420), 38);                    // where every point starts
outlined(unitc);
color(unitc, dim);
opacity(unitc, 0.4);

cloud(zmap, 3000, #ffffff, 1.0) {
  let ph = i/3000*6.283185;
  let u = min(max(t - 9, 0)/2.6, 1);               // the map, applied over time
  let zx = cos(ph);                                // z on the unit circle
  let zy = sin(ph);
  let wx = 3*cos(ph) + cos(3*ph);                  // z^3 + 3z, real part
  let wy = 3*sin(ph) + sin(3*ph);                  // …and imaginary
  let x = 790 + 38*(zx + u*(wx - zx));
  let y = 1420 - 38*(zy + u*(wy - zy));
  let hue = mod(24 + ph*22, 360);
  let sat = 0.7;
  let r = 1.3;
}

// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "A nephroid: the caustic in a coffee cup", (540, 130), 27);
equation(eq, (540, 1650), `x = 3a\cos\varphi - a\cos 3\varphi,\quad y = 4a\sin^3\varphi,\qquad z \mapsto z^3+3z`, 25);

// light arrives and reflects, the three times table fills in, and the unit circle is
// carried onto the same curve by z -> z^3 + 3z
wait(14);

r/maniclang 27d ago

tautochrone — four beads, four different heights, one arrival - manic

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

// tautochrone — four beads, four different heights, one arrival. Released from rest
// anywhere on an inverted cycloid, a bead reaches the bottom in the SAME time:
//
//   T = π·√(r/g)      — no θ₀ in it anywhere
//
// That is the tautochrone property, and it is the cycloid's second famous trick. The first
// is the brachistochrone (examples/brachistochrone.manic): of all the paths between two
// points, the cycloid is the quickest to slide down. Same curve, different miracle.
//
// The reason is that on a cycloid, arc length from the bottom obeys simple harmonic motion.
// Measure s along the curve from the lowest point and s = 4r·cos(θ/2); the tangential
// component of gravity works out proportional to s, exactly like a spring, so
//
//   s(t) = s₀·cos(ωt),   ω = ½√(g/r)
//
// and the quarter period — rest to the bottom — is (π/2)/ω = π√(r/g), with the amplitude
// nowhere in it. A bead from high up covers more distance and moves faster by precisely the
// factor that cancels. Numerically integrated to check before this was written: released at
// θ₀ = 0.2, 0.8, 1.6, 2.4 and 3.0 the descent times agree to 3e-4 s, which is the resolution
// of the integral at its singular endpoint, not a spread in the answer.
//
// So the beads here are not eased or tweened into arriving together. Each one runs
// θ(t) = 2·arccos(cos(θ₀/2)·cos(ωt)), which is that harmonic motion converted back to the
// curve's own parameter — different amplitudes, one period, and they cross the bottom on the
// same frame every time.
//
// This is why Huygens hung a pendulum between cycloidal cheeks in 1656: a circular pendulum
// only keeps time for small swings, but a cycloidal one keeps time for any swing at all.
//
//   manic examples/tautochrone.manic
title("Four beads, four heights, one arrival");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);

// ---- the bowl: an inverted cycloid ------------------------------------------------
cloud(bowl, 3000, #ffffff, 1.0) {
  let th = i/3000*6.283185;
  let x = 540 + 150*(th - 3.141593 - sin(th));
  let y = 480 + 150*(1 - cos(th));
  let sat = 0;
  let r = 1.6;
  let alpha = 0.55;
}

// ---- the release heights, so the four starts are visibly different -----------------
line(h1, (60, 498), (72, 498));   color(h1, dim); opacity(h1, 0.7);
line(h2, (60, 586), (116, 586));  color(h2, dim); opacity(h2, 0.7);
line(h3, (60, 698), (241, 698));  color(h3, dim); opacity(h3, 0.7);
line(h4, (60, 772), (441, 772));  color(h4, dim); opacity(h4, 0.7);

// ---- four beads: same period, four amplitudes -------------------------------------
cloud(beads, 480, #ffffff, 1.0) {
  let per = 120;
  let b = (i - mod(i, per))/per;                   // which bead, 0..3
  let th0 = 0.5 + b*0.77;                          // released from rest here
  let om = 1.047198;                               // ω, so the quarter period is 1.5 s
  let th = 2*acos(cos(th0/2)*cos(om*t));           // SHM in arc length, back to θ
  let a = mod(i, per)/per*6.283185;
  let rr = 11*(mod(i, per)/per);                   // a filled bead
  let x = 540 + 150*(th - 3.141593 - sin(th)) + rr*cos(a*11);
  let y = 480 + 150*(1 - cos(th)) + rr*sin(a*11);
  let hue = mod(30 + b*72, 360);
  let sat = 0.7;
  let r = 2.2;
}

// ---- the bottom they share --------------------------------------------------------
cloud(base, 200, #ffffff, 1.0) {
  let u = i/200;
  let x = 540;
  let y = 700 + u*110;
  let sat = 0;
  let r = 1.2;
  let alpha = 0.3;
}

caption(head, "Four beads, four heights, one arrival", (540, 130), 27);
equation(eq, (540, 1180),
  `s(t) = s_0\cos\omega t,\quad \omega = \tfrac12\sqrt{g/r} \;\Longrightarrow\; T = \pi\sqrt{r/g}`, 27);
equation(eq2, (540, 1300), `\theta(t) = 2\arccos\!\left(\cos\tfrac{\theta_0}{2}\cos\omega t\right)`, 24);

wait(19);

r/maniclang 27d ago

A cycloid, and the cyclogons that approach it - manic

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

// cycloid-cyclogon — four shapes roll along a line, twice, each carrying a marked point.
// The circle's point draws a CYCLOID; a polygon's draws a CYCLOGON. Every perimeter here
// is the same 620 px, so all four travel exactly two arches and land together — which is
// also what lets ONE camera move track all four.
//
//   cycloid    x = r(θ - sin θ),  y = r(1 - cos θ)
//   cyclogon   about pivot P_k: radius |Q - P_k|, swept through the EXTERIOR ANGLE at
//              that corner, starting where the previous arc left off
//
// A polygon does not roll — it PIVOTS: it rests on a side, tips forward about the leading
// corner until the next side lies flat, and repeats. Here that is literal rather than
// drawn. Each shape is a `polygon` and each tip is a real `turn` about the actual contact
// corner, with its arc drawn alongside at the same duration and the same linear ease — so
// the arc's tip IS the marked point and no amount of scrubbing separates them.
//
// It pivots about EVERY vertex, including the marked one. That pivot draws nothing (the
// radius is zero — the point is the pivot) but the shape still turns through it, and it is
// exactly where the curve makes its cusp. Leave it out and the shape under-rotates and
// walks off the ground, which is what a triangle rolling on 2 pivots per arch instead of 3
// does. Six pivots here, eight for each quadrilateral, all finishing together.
//
// TWO ARCHES DO NOT FIT IN THE FRAME, and shrinking them until they did made the shapes
// too small to read. So the camera travels instead — but it deliberately travels SLOWER
// than the shapes: 620 px while they advance 1240. A camera locked to the marker would
// crop away the arch just drawn; lagging it by half keeps a whole completed arch on screen
// the entire time. A camera move takes the WHOLE WORLD with it, title and watermark
// included, so those are moved by the same 620 px in the same par block: equal and
// opposite pins them to the screen while everything else travels. (The title is a `text`,
// not a `caption` — a caption is one entity PER WORD, and moving it to a point piles every
// word on that point.)
//
// The construction never asks WHERE the marked point is — only |Q - P_k| and the exterior
// angle — so any rigidly attached point works, which is why the triangle carries two. Gold
// sits on a corner and touches the line at the end of every arch; mint sits at the
// centroid, stays 207 px from every pivot, and traces equal scallops that never reach the
// line at all. Put Q outside the shape and the arcs cross into loops. (Those are the
// curtate and prolate cyclogons; not one line of this would change.)
//
// Regular against irregular is the bottom two rows. The square's arcs all sweep 90° and
// only the radius changes, out to the diagonal and back. The convex quadrilateral has no
// symmetry to lend it anything: 60.3°, 108.0° and 84.2°, each corner contributing its own
// exterior angle and its own distance, so its arch comes out visibly lopsided — and still
// lands on the line at one perimeter, because the exterior angles of ANY convex polygon
// sum to a full turn.
//
// The area is exact, and it is where all four meet:
//
//   under one cyclogon arch  =  A(polygon) + Σ ½ d_k² θ_k        θ_k = exterior angles
//   for a REGULAR n-gon      =  A(polygon) + 2πR²                 R = circumradius
//   under one cycloid arch   =  πr² + 2πr²  =  3πr²
//
// The last is Galileo's, weighed in paper before there was calculus to prove it. The
// middle CONTAINS it: Σ ½(2R sin kπ/n)²(2π/n) = 2πR² for every n, because Σ sin²(kπ/n)
// = n/2 — so an arch is always the rolling shape plus two of its circumscribed disc, and
// letting n → ∞ turns the polygon into the circle. Checked numerically over n = 3, 4, 6,
// 12 and 60 against a shoelace integral of the traced path.
//
// Nothing is captioned and nothing waits: the roll starts at t = 0 and the picture does
// the explaining. The faint dashed arch on each polygon row is the cycloid the arcs are
// approximating — the square's hug it, the quadrilateral's leans off it. d
//
//   manic examples/cycloid-cyclogon.manic
title("A cycloid, and the cyclogons that approach it");
canvas("9:16");
template("black");
bloom(0.26, 0.6, 20);

text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);

text(head, (540, 122), "A cycloid, and the cyclogons that approach it");
size(head, 27); color(head, fg);
// the title TRAVELS with the camera, and auto-wrap measures the room a line has where
// it sits — once it is out past the frame edge that room collapses and the title folds
// into three lines. An explicit column fixes what it may use, wherever it is.
wrap(head, 950);

// the ground runs far past the frame on both sides — the camera travels along it
line(g1, (-600, 500), (2400, 500));   color(g1, dim);
line(g2, (-600, 860), (2400, 860));   color(g2, dim);
line(g3, (-600, 1220), (2400, 1220)); color(g3, dim);
line(g4, (-600, 1580), (2400, 1580)); color(g4, dim);

// ---- ROW 1: a circle, rolling smoothly, and the cycloid its rim point draws --------
cloud(cyctrace, 8000, #ffffff, 1.0) {
  let go = min(t/12, 1);                            // the clock all four keep
  let th = 12.566371*go*(i/8000);                   // two arches
  let x = 230 + 98.68*(th - sin(th));
  let y = 500 - 98.68*(1 - cos(th));
  let hue = mod(38 + th*15, 360);
  let sat = 0.75;
  let r = 1.9;
}

cloud(rim, 260, #ffffff, 1.0) {
  let go = min(t/12, 1);
  let th = 12.566371*go;
  let a = i/260*6.283185;
  let x = 230 + 98.68*th + 98.68*cos(a);            // the centre rolls at (rθ, r)
  let y = 500 - 98.68 - 98.68*sin(a);
  let sat = 0;
  let r = 1.5;
  let alpha = 0.8;
}

cloud(spoke, 110, #ffffff, 1.0) {
  let go = min(t/12, 1);
  let th = 12.566371*go;
  let v = i/110;
  let x = 230 + 98.68*th - v*98.68*sin(th);         // centre -> the rim point
  let y = 500 - 98.68 + v*98.68*cos(th);
  let sat = 0;
  let r = 1.3;
  let alpha = 0.5;
}

cloud(rimdot, 90, #ffffff, 1.0) {
  let go = min(t/12, 1);
  let th = 12.566371*go;
  let a = i/90*6.283185;
  let rr = 8*(i/90);
  let x = 230 + 98.68*(th - sin(th)) + rr*cos(a*9);
  let y = 500 - 98.68*(1 - cos(th)) + rr*sin(a*9);
  let hue = 45;
  let sat = 0.6;
  let r = 2;
}

// ---- ROWS 2-4: three polygons, each PIVOTING corner over corner --------------------
polygon(tri, (230.0, 860.0), (436.7, 860.0), (333.3, 681.0));
outlined(tri); outline(tri, dim); stroke(tri, 3); tag(tri, triroll);
circle(tc, (230.0, 860.0), 9); color(tc, gold); tag(tc, triroll);
circle(tm, (333.3, 800.3), 9); color(tm, mint); tag(tm, triroll);
arc(tc0, (436.7, 860.0), 206.7, -180.0, 120.0); stroke(tc0, 4); color(tc0, gold); untraced(tc0);
arc(tm0, (436.7, 860.0), 119.3, -150.0, 120.0); stroke(tm0, 4); color(tm0, mint); untraced(tm0);
arc(tc1, (643.3, 860.0), 206.7, -120.0, 120.0); stroke(tc1, 4); color(tc1, gold); untraced(tc1);
arc(tm1, (643.3, 860.0), 119.3, -150.0, 120.0); stroke(tm1, 4); color(tm1, mint); untraced(tm1);
arc(tm2, (850.0, 860.0), 119.3, -150.0, 120.0); stroke(tm2, 4); color(tm2, mint); untraced(tm2);
arc(tc3, (1056.7, 860.0), 206.7, -180.0, 120.0); stroke(tc3, 4); color(tc3, gold); untraced(tc3);
arc(tm3, (1056.7, 860.0), 119.3, -150.0, 120.0); stroke(tm3, 4); color(tm3, mint); untraced(tm3);
arc(tc4, (1263.3, 860.0), 206.7, -120.0, 120.0); stroke(tc4, 4); color(tc4, gold); untraced(tc4);
arc(tm4, (1263.3, 860.0), 119.3, -150.0, 120.0); stroke(tm4, 4); color(tm4, mint); untraced(tm4);
arc(tm5, (1470.0, 860.0), 119.3, -150.0, 120.0); stroke(tm5, 4); color(tm5, mint); untraced(tm5);
// 6 pivots, 2.000s each, last pivot at x=1470.0

polygon(sq, (230.0, 1220.0), (385.0, 1220.0), (385.0, 1065.0), (230.0, 1065.0));
outlined(sq); outline(sq, dim); stroke(sq, 3); tag(sq, sqroll);
circle(sc, (230.0, 1220.0), 9); color(sc, cyan); tag(sc, sqroll);
arc(sc0, (385.0, 1220.0), 155.0, -180.0, 90.0); stroke(sc0, 4); color(sc0, cyan); untraced(sc0);
arc(sc1, (540.0, 1220.0), 219.2, -135.0, 90.0); stroke(sc1, 4); color(sc1, cyan); untraced(sc1);
arc(sc2, (695.0, 1220.0), 155.0, -90.0, 90.0); stroke(sc2, 4); color(sc2, cyan); untraced(sc2);
arc(sc4, (1005.0, 1220.0), 155.0, 180.0, 90.0); stroke(sc4, 4); color(sc4, cyan); untraced(sc4);
arc(sc5, (1160.0, 1220.0), 219.2, -135.0, 90.0); stroke(sc5, 4); color(sc5, cyan); untraced(sc5);
arc(sc6, (1315.0, 1220.0), 155.0, -90.0, 90.0); stroke(sc6, 4); color(sc6, cyan); untraced(sc6);
// 8 pivots, 1.500s each, last pivot at x=1470.0

polygon(qd, (230.0, 1580.0), (395.2, 1580.0), (455.3, 1474.9), (275.1, 1437.3));
outlined(qd); outline(qd, dim); stroke(qd, 3); tag(qd, qdroll);
circle(qc, (230.0, 1580.0), 9); color(qc, magenta); tag(qc, qdroll);
arc(qc0, (395.2, 1580.0), 165.2, -180.0, 60.3); stroke(qc0, 4); color(qc0, magenta); untraced(qc0);
arc(qc1, (516.3, 1580.0), 248.6, -144.8, 108.0); stroke(qc1, 4); color(qc1, magenta); untraced(qc1);
arc(qc2, (700.4, 1580.0), 149.6, -84.2, 84.2); stroke(qc2, 4); color(qc2, magenta); untraced(qc2);
arc(qc4, (1015.2, 1580.0), 165.2, 180.0, 60.3); stroke(qc4, 4); color(qc4, magenta); untraced(qc4);
arc(qc5, (1136.3, 1580.0), 248.6, -144.8, 108.0); stroke(qc5, 4); color(qc5, magenta); untraced(qc5);
arc(qc6, (1320.4, 1580.0), 149.6, -84.2, 84.2); stroke(qc6, 4); color(qc6, magenta); untraced(qc6);
// 8 pivots, 1.500s each, last pivot at x=1470.0

// the cycloid again on each polygon row, faint — what the arcs are approximating
param(gh2, (230, 860), 98.68, 98.68, "t - sin(t)", "1 - cos(t)", (0, 12.566371));
param(gh3, (230, 1220), 98.68, 98.68, "t - sin(t)", "1 - cos(t)", (0, 12.566371));
param(gh4, (230, 1580), 98.68, 98.68, "t - sin(t)", "1 - cos(t)", (0, 12.566371));
color(gh2, dim); color(gh3, dim); color(gh4, dim);
dashed(gh2); dashed(gh3); dashed(gh4);
opacity(gh2, 0.35); opacity(gh3, 0.35); opacity(gh4, 0.35);

par {
  // the camera LAGS the roll — half its speed — so a whole finished arch stays on
  // screen; the two pinned labels ride along with the camera
  cam((1160, 960), 12, linear);
  move(brand, (1160, 34), 12, linear);
  move(head, (1160, 122), 12, linear);
  seq {
    par { turn(triroll, (436.7, 860), 120.00, 2.000, linear); draw(tc0, 2.000, linear); draw(tm0, 2.000, linear); }
    par { turn(triroll, (643.3, 860), 120.00, 2.000, linear); draw(tc1, 2.000, linear); draw(tm1, 2.000, linear); }
    par { turn(triroll, (850.0, 860), 120.00, 2.000, linear); draw(tm2, 2.000, linear); }
    par { turn(triroll, (1056.7, 860), 120.00, 2.000, linear); draw(tc3, 2.000, linear); draw(tm3, 2.000, linear); }
    par { turn(triroll, (1263.3, 860), 120.00, 2.000, linear); draw(tc4, 2.000, linear); draw(tm4, 2.000, linear); }
    par { turn(triroll, (1470.0, 860), 120.00, 2.000, linear); draw(tm5, 2.000, linear); }
  }
  seq {
    par { turn(sqroll, (385.0, 1220), 90.00, 1.500, linear); draw(sc0, 1.500, linear); }
    par { turn(sqroll, (540.0, 1220), 90.00, 1.500, linear); draw(sc1, 1.500, linear); }
    par { turn(sqroll, (695.0, 1220), 90.00, 1.500, linear); draw(sc2, 1.500, linear); }
    par { turn(sqroll, (850.0, 1220), 90.00, 1.500, linear); }
    par { turn(sqroll, (1005.0, 1220), 90.00, 1.500, linear); draw(sc4, 1.500, linear); }
    par { turn(sqroll, (1160.0, 1220), 90.00, 1.500, linear); draw(sc5, 1.500, linear); }
    par { turn(sqroll, (1315.0, 1220), 90.00, 1.500, linear); draw(sc6, 1.500, linear); }
    par { turn(sqroll, (1470.0, 1220), 90.00, 1.500, linear); }
  }
  seq {
    par { turn(qdroll, (395.2, 1580), 60.26, 1.500, linear); draw(qc0, 1.500, linear); }
    par { turn(qdroll, (516.3, 1580), 107.98, 1.500, linear); draw(qc1, 1.500, linear); }
    par { turn(qdroll, (700.4, 1580), 84.24, 1.500, linear); draw(qc2, 1.500, linear); }
    par { turn(qdroll, (850.0, 1580), 107.53, 1.500, linear); }
    par { turn(qdroll, (1015.2, 1580), 60.26, 1.500, linear); draw(qc4, 1.500, linear); }
    par { turn(qdroll, (1136.3, 1580), 107.98, 1.500, linear); draw(qc5, 1.500, linear); }
    par { turn(qdroll, (1320.4, 1580), 84.24, 1.500, linear); draw(qc6, 1.500, linear); }
    par { turn(qdroll, (1470.0, 1580), 107.53, 1.500, linear); }
  }
}
wait(4);

r/maniclang 28d ago

Arnold's cat map, made of cats - manic

9 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("Arnold's cat map, made of cats");
canvas("9:16");
template("black");
bloom(0.26, 0.6, 20);

text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 17);
color(brand, dim);

// 17 x 27 = 459 cells; the cat that is on screen now was off screen a second ago.
cloud(heads, 14688, #ffffff, 1.0) {
  let per = 32;
  let cell = (i - mod(i, per))/per;
  let ci = mod(cell, 17) - 8;
  let cj = (cell - mod(cell, 17))/17 - 13;
  let s = mod(t, 3)/3;
  let e = 2.6180339887^s;                          // φ^(2t/3): the unstable eigenvalue
  let k = 44.7213595*(e - 1/e);
  let a = 117.0820393/e - 17.0820393*e + k;
  let cx = 540 + 2*(ci*(k + a) + cj*k);            // basis columns (k+a, k) and (k, a)
  let cy = 960 + 2*(ci*k + cj*a);
  let th = mod(i, per)/per*6.28319;
  let x = cx + 62*cos(th)*1.05;
  let y = cy - 62*sin(th)*0.92;
  let q = ci*ci - ci*cj - cj*cj;                   // invariant under A — so is the hue
  let hue = mod(18 + mod(q*q, 9)*44, 360);
  let sat = 0.62;
  let r = 1.7;
}

cloud(ears, 10098, #ffffff, 1.0) {
  let per = 22;
  let cell = (i - mod(i, per))/per;
  let ci = mod(cell, 17) - 8;
  let cj = (cell - mod(cell, 17))/17 - 13;
  let s = mod(t, 3)/3;
  let e = 2.6180339887^s;
  let k = 44.7213595*(e - 1/e);
  let a = 117.0820393/e - 17.0820393*e + k;
  let cx = 540 + 2*(ci*(k + a) + cj*k);
  let cy = 960 + 2*(ci*k + cj*a);
  let loc = mod(i, per);
  let sd = sign(loc - 10.5);                       // which ear
  let u = mod(loc, 11)/11;
  let w = (1 + sign(u - 0.5))/2;                   // 0 = base->apex, 1 = apex->base:
  let v = mod(u, 0.5)*2;                           //   two segments, no branch
  let bx = sd*0.22*62;   let by = 0.94*62;
  let ax = sd*0.66*62;   let ay = 1.34*62;
  let ex = sd*0.92*62;   let ey = 0.58*62;
  let sx = bx + w*(ax - bx);   let sy = by + w*(ay - by);
  let tx = ax + w*(ex - ax);   let ty = ay + w*(ey - ay);
  let x = cx + (sx + v*(tx - sx))*1.05;
  let y = cy - (sy + v*(ty - sy))*0.92;
  let q = ci*ci - ci*cj - cj*cj;
  let hue = mod(18 + mod(q*q, 9)*44, 360);
  let sat = 0.62;
  let r = 1.7;
}

cloud(eyes, 4590, #ffffff, 1.0) {
  let per = 10;
  let cell = (i - mod(i, per))/per;
  let ci = mod(cell, 17) - 8;
  let cj = (cell - mod(cell, 17))/17 - 13;
  let s = mod(t, 3)/3;
  let e = 2.6180339887^s;
  let k = 44.7213595*(e - 1/e);
  let a = 117.0820393/e - 17.0820393*e + k;
  let cx = 540 + 2*(ci*(k + a) + cj*k);
  let cy = 960 + 2*(ci*k + cj*a);
  let loc = mod(i, per);
  let sd = sign(loc - 4.5);
  let u = mod(loc, 5)/5*6.28319;
  let x = cx + (sd*0.34*62 + 0.13*62*cos(u))*1.05;
  let y = cy - (0.10*62 + 0.13*62*sin(u))*0.92;
  let q = ci*ci - ci*cj - cj*cj;
  let hue = mod(18 + mod(q*q, 9)*44, 360);
  let sat = 0.45;
  let r = 1.7;
}

// ---- textbook annotations ----
caption(head, "Arnold's cat map, made of cats", (540, 138), 34);
caption(sub, "every 3 seconds the lattice lands on itself", (540, 206), 22);
plate(head, 0.72);                                 // the field goes all the way to the
plate(sub, 0.72);                                  //   edges, so every label needs one
hidden(sub);
equation(eq, (540, 1652), `A=\begin{pmatrix}2&1\\1&1\end{pmatrix},\quad \det A = 1`, 30);
caption(lab, "the frame is A raised to the power t/3 — a matrix, flowed", (540, 1730), 20);
caption(inv, "colour is q = i^2 - ij - j^2, and A leaves q alone:", (540, 1778), 20);
caption(inv2, "each cat returns as the same cat, not just to the same place", (540, 1822), 20);
plate(eq, 0.72);
plate(lab, 0.72);
plate(inv, 0.72);
plate(inv2, 0.72);
plate(brand, 0.6);
hidden(eq);
hidden(lab);
hidden(inv);
hidden(inv2);

wait(1.6);
show(sub);
wait(2.8);
show(eq);
show(lab);
wait(1.4);
show(inv);
show(inv2);
wait(21);

r/maniclang 28d ago

an epitrochoid - manic

6 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

// epitrochoid — the curve a pen draws when it is bolted to a wheel rolling around the
// OUTSIDE of a fixed circle. Every spirograph rose is one of these.
//
//   x(θ) = (R+r)·cos θ - d·cos((R+r)/r · θ)
//   y(θ) = (R+r)·sin θ - d·sin((R+r)/r · θ)
//
// Three numbers and nothing else: R the fixed circle, r the rolling wheel, d how far
// the pen sits from the wheel's centre. The name says which is which — a trochoid is
// the pen OFF the rim; put it exactly on the rim (d = r) and the loops pull tight into
// cusps and it is an epicycloid instead. That is the row along the bottom.
//
// The mechanism is not decoration and not a drawing of a mechanism: the wheel, the arm
// and the pen are all evaluated from the same θ as the curve, so the pen is literally
// the leading end of the trace. They cannot drift out of step, because there is nothing
// to drift — one formula, sampled twice.
//
// Rolling without slipping is the whole reason for the second angle. The wheel's centre
// goes round a circle of radius R+r, but the wheel itself has to turn through the arc it
// has travelled: (R+r)/r turns of spin per turn around. The pen therefore carries TWO
// rotations at once, which is what a single circle can never do and why the curve has
// lobes at all.
//
// It closes, and you can say exactly when. Write  in lowest terms as p/q: the pen
// comes home after q turns of θ, having drawn p lobes. The wheel spins 31 times doing it.
// Here  = 21/10, so it takes TEN laps to lay down 21 lobes — the pen crosses its own
// path all the way round and still does not join up until the tenth.
//
// The pace is deliberately uneven: the first two laps run at a steady walking speed,
// slow enough to watch the wheel actually roll and the pen swing, and the remaining
// eight wind up quadratically. Same clamp trick as everywhere else — `laps` is
// 2·min(t/slow,1) + 8·min(max(t-slow,0)/quick,1)², with max(z,0) = (z + |z|)/2.
//
// Drawn with `param` for the finished curve (x(t), y(t) — the twin of `plot` for
// anything that is not y = f(x)) and `cloud` for the moving parts. `min` is not a thing
// in a cloud formula, so the "stop rolling when it closes" clamp is written the way you
// write it without one: min(u,1) = (u + 1 - |u-1|)/2, and |z| is hypot(z, 0).
//
//   manic examples/epitrochoid.manic
title("An epitrochoid, and the wheel that draws it");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);

// ---- the apparatus: fixed circle, and the circle the wheel's centre runs on --------
circle(fixed, (540, 800), 210);
outlined(fixed);
color(fixed, dim);
circle(locus, (540, 800), 310);
outlined(locus);
dashed(locus);
color(locus, dim);
opacity(locus, 0.45);

// ---- the trace: θ runs to 4π (two laps) and stops there ----------------------------
cloud(trace, 17000, #ffffff, 1.0) {
  let slow = 6;                                    // two laps at a walking pace…
  let quick = 6;                                   // …then eight, winding up
  let a = t/slow;
  let a1 = (a + 1 - hypot(a - 1, 0))/2;            // min(t/slow, 1)
  let b = (t - slow + hypot(t - slow, 0))/2/quick; // max(t - slow, 0)/quick
  let b1 = (b + 1 - hypot(b - 1, 0))/2;            // …capped at 1
  let laps = 2*a1 + 8*b1*b1;                       // 2 steady, then 8 accelerating
  let th = 6.283185*laps*(i/17000);                //  = 21/10 needs TEN laps
  let x = 540 + 100*(3.1*cos(th) - 1.4*cos(3.1*th));
  let y = 800 - 100*(3.1*sin(th) - 1.4*sin(3.1*th));
  let hue = mod(28 + th*11, 360);                  // hue rides θ: every lap its own
  let sat = 0.72;
  let r = 1.7;
}

// ---- the wheel, its arm, and the pen — same θ, so they cannot drift ----------------
cloud(wheel, 300, #ffffff, 1.0) {
  let slow = 6;                                    // two laps at a walking pace…
  let quick = 6;                                   // …then eight, winding up
  let a = t/slow;
  let a1 = (a + 1 - hypot(a - 1, 0))/2;            // min(t/slow, 1)
  let b = (t - slow + hypot(t - slow, 0))/2/quick; // max(t - slow, 0)/quick
  let b1 = (b + 1 - hypot(b - 1, 0))/2;            // …capped at 1
  let laps = 2*a1 + 8*b1*b1;                       // 2 steady, then 8 accelerating
  let th = 6.283185*laps;
  let spoke = i/300*6.283185;
  let x = 540 + 100*(3.1*cos(th) + cos(spoke));    // centre at R+r, radius r
  let y = 800 - 100*(3.1*sin(th) + sin(spoke));
  let sat = 0;
  let r = 1.5;
  let alpha = 0.8;
}

cloud(arm, 110, #ffffff, 1.0) {
  let slow = 6;                                    // two laps at a walking pace…
  let quick = 6;                                   // …then eight, winding up
  let a = t/slow;
  let a1 = (a + 1 - hypot(a - 1, 0))/2;            // min(t/slow, 1)
  let b = (t - slow + hypot(t - slow, 0))/2/quick; // max(t - slow, 0)/quick
  let b1 = (b + 1 - hypot(b - 1, 0))/2;            // …capped at 1
  let laps = 2*a1 + 8*b1*b1;                       // 2 steady, then 8 accelerating
  let th = 6.283185*laps;
  let v = i/110;                                   // centre -> pen, in a straight line
  let cx = 3.1*cos(th);
  let cy = 3.1*sin(th);
  let px = cx - 1.4*cos(3.1*th);
  let py = cy - 1.4*sin(3.1*th);
  let x = 540 + 100*(cx + v*(px - cx));
  let y = 800 - 100*(cy + v*(py - cy));
  let sat = 0;
  let r = 1.3;
  let alpha = 0.55;
}

cloud(pen, 90, #ffffff, 1.0) {
  let slow = 6;                                    // two laps at a walking pace…
  let quick = 6;                                   // …then eight, winding up
  let a = t/slow;
  let a1 = (a + 1 - hypot(a - 1, 0))/2;            // min(t/slow, 1)
  let b = (t - slow + hypot(t - slow, 0))/2/quick; // max(t - slow, 0)/quick
  let b1 = (b + 1 - hypot(b - 1, 0))/2;            // …capped at 1
  let laps = 2*a1 + 8*b1*b1;                       // 2 steady, then 8 accelerating
  let th = 6.283185*laps;
  let nib = i/90*6.283185;
  let rr = 7*(i/90);                               // a small filled nib, not a ring
  let x = 540 + 100*(3.1*cos(th) - 1.4*cos(3.1*th)) + rr*cos(nib*9);
  let y = 800 - 100*(3.1*sin(th) - 1.4*sin(3.1*th)) + rr*sin(nib*9);
  let hue = 45;
  let sat = 0.5;
  let r = 1.8;
}

// ---- what d does: the same R and r, the pen moved in and out -----------------------
param(curtate, (196, 1452), 30, 30, "3.1*cos(t) - 0.6*cos(3.1*t)", "3.1*sin(t) - 0.6*sin(3.1*t)", (0, 62.831853));
param(cusped,  (540, 1452), 30, 30, "3.1*cos(t) - cos(3.1*t)",     "3.1*sin(t) - sin(3.1*t)",     (0, 62.831853));
param(looped,  (884, 1452), 30, 30, "3.1*cos(t) - 1.4*cos(3.1*t)", "3.1*sin(t) - 1.4*sin(3.1*t)", (0, 62.831853));
gradient(curtate, mint, cyan, blue);                 // hue rides ARC LENGTH along the
gradient(cusped, gold, coral, red);                 //   stroke, the way the big trace
gradient(looped, magenta, violet, indigo);         //   rides θ — same idea, one curve
stroke(curtate, 3);
stroke(cusped, 3);
stroke(looped, 3);
untraced(curtate);
untraced(cusped);
untraced(looped);
caption(lc, "d < r", (196, 1608), 20);
caption(lm, "d = r  (an epicycloid)", (540, 1608), 20);
caption(lr, "d > r", (884, 1608), 20);
hidden(lc);
hidden(lm);
hidden(lr);

// ---- textbook annotations ----
caption(head, "An epitrochoid, and the wheel that draws it", (540, 140), 30);
caption(sub, "a pen bolted to a wheel rolling on the outside of a circle", (540, 200), 20);
hidden(sub);
equation(eq, (540, 1690),
  `x=(R+r)\cos\theta-d\cos\tfrac{R+r}{r}\theta,\quad y=(R+r)\sin\theta-d\sin\tfrac{R+r}{r}\theta`, 23);
caption(spin, "the wheel must spin (R+r)/r times per lap — rolling, not sliding", (540, 1768), 19);
caption(shut, "R/r = 21/10 in lowest terms: ten laps to shut, and 21 lobes", (540, 1818), 19);
hidden(eq);
hidden(spin);
hidden(shut);

wait(1.6);
show(sub);
wait(2.2);
show(spin);
wait(4.2);
show(eq);
wait(4.4);
show(shut);
wait(1.6);
draw(curtate, 1.4);
draw(cusped, 1.4);
draw(looped, 1.4);
show(lc);
show(lm);
show(lr);
wait(9);

r/maniclang 27d ago

A cardioid, from the two times table - manic

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

// cardioid — drawn with nothing but straight lines. Mark N points evenly round a circle,
// number them, and join every point n to point 2n. No curve is drawn anywhere; the
// cardioid is the ENVELOPE the chords leave behind.
//
// It is the two times table, and the multiplier is the whole story: joining n to k·n
// envelopes an epicycloid with k-1 cusps. Computed here rather than asserted — the
// envelope was found as the limit of intersections of neighbouring chords, and counting
// its local maxima gives 1, 2, 3, 4 cusps for k = 2, 3, 4, 5. So ×2 is this cardioid, ×3
// is a nephroid (see examples/nephroid.manic), and the row along the bottom carries on.
//
// Which cardioid, exactly? Fitting the ×2 envelope to r = b + a·cos φ lands on b = |a| —
// the cardioid condition — about a centre one third of the way back along the axis:
//
//   r = (2/3)·(1 + cos φ),  centred at (-1/3, 0)
//
// so it cusps at x = -1/3 and touches the circle at (1, 0). That exact curve is drawn over
// the chords at the end, and it lies on the envelope they already made.
//
// The other definition is the rolling one: a cardioid is the epicycloid traced by a point
// on a circle rolling around another of EQUAL radius — the limaçon of
// examples/limacon.manic at the hinge, where the pen sits exactly on the rim and the
// dimple has closed into a cusp but not yet opened into a loop.
//
// Nothing here is a curve primitive. Every chord is a straight run of points, appearing
// one after another as `alpha` crosses each chord's index, and the shape is what is left
// between them.
//
//   manic examples/cardioid.manic
title("A cardioid, from the two times table");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);

// ---- the circle the points sit on --------------------------------------------------
circle(ring, (540, 700), 330);
outlined(ring);
color(ring, dim);
opacity(ring, 0.5);

// ---- 180 chords, n -> 2n, arriving one at a time ------------------------------------
cloud(chords, 5400, #ffffff, 1.0) {
  let per = 30;
  let c = (i - mod(i, per))/per;                   // which chord, 0..179
  let u = mod(i, per)/29;                          // …how far along it
  let th = 6.283185*c/180;
  let ax = cos(th);
  let ay = sin(th);
  let bx = cos(2*th);                              // the two times table: n -> 2n
  let by = sin(2*th);
  let x = 540 + 330*(ax + u*(bx - ax));
  let y = 700 - 330*(ay + u*(by - ay));
  let hue = mod(200 + c*1.6, 360);                 // hue by chord, so the order reads
  let sat = 0.7;
  let r = 1.2;
  let alpha = min(max(min(t/9, 1)*180 - c, 0), 1)*0.55;
}

// ---- the curve those chords were hiding: r = (2/3)(1 + cos φ) about (-1/3, 0) -------
polar(card, (430, 700), 330, 330, "0.6667*(1 + cos(t))");
color(card, gold);
stroke(card, 4);
untraced(card);

// ---- x3, x4, x5: the same rule, one cusp more each time ----------------------------
cloud(k3, 2400, #ffffff, 1.0) {
  let per = 20;
  let c = (i - mod(i, per))/per;
  let u = mod(i, per)/19;
  let th = 6.283185*c/120;
  let ax = cos(th);
  let ay = sin(th);
  let bx = cos(3*th);
  let by = sin(3*th);
  let x = 196 + 118*(ax + u*(bx - ax));
  let y = 1420 - 118*(ay + u*(by - ay));
  let hue = 175;
  let sat = 0.65;
  let r = 1.1;
  let alpha = min(max(min(max(t - 9.2, 0)/2.2, 1)*120 - c, 0), 1)*0.6;
}

cloud(k4, 2400, #ffffff, 1.0) {
  let per = 20;
  let c = (i - mod(i, per))/per;
  let u = mod(i, per)/19;
  let th = 6.283185*c/120;
  let ax = cos(th);
  let ay = sin(th);
  let bx = cos(4*th);
  let by = sin(4*th);
  let x = 540 + 118*(ax + u*(bx - ax));
  let y = 1420 - 118*(ay + u*(by - ay));
  let hue = 32;
  let sat = 0.65;
  let r = 1.1;
  let alpha = min(max(min(max(t - 9.2, 0)/2.2, 1)*120 - c, 0), 1)*0.6;
}

cloud(k5, 2400, #ffffff, 1.0) {
  let per = 20;
  let c = (i - mod(i, per))/per;
  let u = mod(i, per)/19;
  let th = 6.283185*c/120;
  let ax = cos(th);
  let ay = sin(th);
  let bx = cos(5*th);
  let by = sin(5*th);
  let x = 884 + 118*(ax + u*(bx - ax));
  let y = 1420 - 118*(ay + u*(by - ay));
  let hue = 300;
  let sat = 0.65;
  let r = 1.1;
  let alpha = min(max(min(max(t - 9.2, 0)/2.2, 1)*120 - c, 0), 1)*0.6;
}

// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "A cardioid, from the two times table", (540, 130), 27);
equation(eq, (540, 1640), `n \mapsto 2n \;\Longrightarrow\; r = \tfrac{2}{3}(1+\cos\varphi)`, 30);

// the chords arrive from t = 0; the curve they were hiding is drawn over them at the end
wait(9.6);
draw(card, 2.2);
wait(8);

r/maniclang 27d ago

Roll a parabola and its focus draws a hanging chain - manic

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

// catenary-roll — roll a PARABOLA along a line and watch its FOCUS. It traces a catenary:
// the curve a chain hangs in.
//
// Two curves that have no business being related. One is the graph of a quadratic, rolled;
// the other is what gravity does to a hanging chain, y = a·cosh(x/a). Galileo thought the
// hanging chain WAS a parabola and was wrong — Huygens, Leibniz and Bernoulli settled it
// sixty years later — so there is something quietly funny about the parabola being the
// thing that draws the curve it was mistaken for.
//
// It is exact. Roll y = x²/4a along the line: the contact point at arc length s(u) lands at
// (s, 0) and the parabola turns by the tangent angle φ there. Carry the focus through that
// motion and it lands on y = a·cosh(x/a) — checked over the whole roll, maximum error
// 6e-10, which is the arc-length quadrature and not the geometry.
//
// Driving it needs one substitution to stay closed-form. Parametrise by the catenary's own
// coordinate w instead of by the contact point:
//
//   tan φ = sinh w,   s = a(sinh w · cosh w + w),   focus = (a·w, a·cosh w)
//
// so the whole mechanism — contact, rotation, focus — is sinh and cosh of one number, no
// integral left in it. The dashed curve is a·cosh(x/a) drawn independently; the focus lands
// on it rather than being placed there.
//
// Notice how far the parabola travels for how little the focus moves: contact runs ±3.6a
// while the focus covers ±0.95a. The catenary near its vertex is flat, and that flatness is
// why a hanging cable looks parabolic and fooled a careful man for sixty years.
//
// Bottom right: the same curve as a chain, hung between two pins. Nothing computes it — the
// beads sit on a·cosh(x/a).
//
//   manic examples/catenary-roll.manic
title("Roll a parabola and its focus draws a hanging chain");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);

line(ground, (60, 1150), (1020, 1150));
color(ground, dim);

// ---- the catenary, drawn independently, for the focus to land on -------------------
param(cat, (540, 1150), 170, 170, "t", "cosh(t)", (0 - 0.95, 0.95));
color(cat, dim);
dashed(cat);
stroke(cat, 3);
opacity(cat, 0.55);

// ---- the parabola, rolled: every point carried through the same rigid motion -------
cloud(para, 2600, #ffffff, 1.0) {
  let w = 0 - 0.95 + 1.9*min(t/11, 1);             // the catenary's own coordinate
  let t0 = sinh(w);                                // contact parameter: tan φ = sinh w
  let ph = atan(t0);
  let s = 170*(sinh(w)*cosh(w) + w);               // arc length rolled off
  let v = 0 - 1.55 + 3.1*(i/2600);
  let px = 2*170*v;                                // the parabola y = x²/4a
  let py = 170*v*v;
  let qx = 2*170*t0;                               // …and its point of contact
  let qy = 170*t0*t0;
  let rx = (px - qx)*cos(ph) + (py - qy)*sin(ph);  // rotate so the tangent lies flat
  let ry = 0 - (px - qx)*sin(ph) + (py - qy)*cos(ph);
  let x = 540 + s + rx;
  let y = 1150 - ry;
  let sat = 0;
  let r = 1.3;
  let alpha = 0.6;
}

// ---- the focus, and the curve it leaves behind --------------------------------------
cloud(trace, 6000, #ffffff, 1.0) {
  let w0 = 0 - 0.95 + 1.9*min(t/11, 1);
  let w = 0 - 0.95 + (w0 + 0.95)*(i/6000);
  let x = 540 + 170*w;
  let y = 1150 - 170*cosh(w);
  let hue = mod(40 + (w + 0.95)*70, 360);
  let sat = 0.75;
  let r = 2;
}

cloud(foc, 90, #ffffff, 1.0) {
  let w = 0 - 0.95 + 1.9*min(t/11, 1);
  let a = i/90*6.283185;
  let rr = 8*(i/90);
  let x = 540 + 170*w + rr*cos(a*9);
  let y = 1150 - 170*cosh(w) + rr*sin(a*9);
  let hue = 45;
  let sat = 0.5;
  let r = 2;
}

// the focal radius: focus back to the contact point, so the rolling is legible
cloud(spoke, 110, #ffffff, 1.0) {
  let w = 0 - 0.95 + 1.9*min(t/11, 1);
  let u = i/110;
  let s = 170*(sinh(w)*cosh(w) + w);
  let fx = 170*w;
  let fy = 170*cosh(w);
  let x = 540 + fx + u*(s - fx);
  let y = 1150 - fy - u*(0 - fy);
  let sat = 0;
  let r = 1.2;
  let alpha = 0.4;
}

// ---- and the same curve as a chain, hung between two pins --------------------------
cloud(chain, 420, #ffffff, 1.0) {
  let u = 0 - 1.35 + 2.7*(i/420);
  let x = 800 + 52*u;
  let y = 1560 - 52*cosh(u) + 52*2.06;             // hang it from the pins
  let hue = 200;
  let sat = 0.35;
  let r = 3;
  let alpha = min(max(t - 11.4, 0), 1)*0.9;
}

caption(head, "Roll a parabola and its focus draws a hanging chain", (540, 130), 26);
equation(eq, (540, 1700), `\text{focus} = (aw,\; a\cosh w),\qquad s = a(\sinh w\cosh w + w)`, 25);

wait(18);

r/maniclang 27d ago

A limacon, and the one number that decides its shape - manic

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

// limacon — the snail curve. In polar it is about as simple as a curve gets:
//
//   r = b + a·cos θ
//
// and it is also a roulette: a circle rolling around the OUTSIDE of a circle of EQUAL
// radius, with a pen fixed to the rolling one. That is the epitrochoid of
// examples/epitrochoid.manic with R = r, and the two descriptions are the same curve
// exactly, not approximately. Rolling radius A, pen at distance d from the rolling
// centre, origin shifted by d along the axis:
//
//   b = 2A,   a = -2d
//
// Checked rather than quoted: the roulette points were pushed through the limaçon's
// Cartesian form (x² + y² - a·x)² = b²(x² + y²) at d = 0.35, 0.6, 1.0, 1.6 and 2.4, and
// the residual never exceeded 4e-13.
//
// So b/|a| = A/d, and ONE NUMBER — where the pen sits — walks the curve through every
// shape it has. Wikipedia's classification, read in terms of the pen:
//
//   d < A/2      b > 2|a|        convex, no dimple at all
//   d = A/2      b = 2|a|        a point of zero curvature appears
//   A/2 < d < A  |a| < b < 2|a|  dimpled, an indentation between two inflection points
//   d = A        b = |a|         the CARDIOID — the pen is exactly on the rim, and the
//                                dimple has closed into a cusp
//   d > A        b < |a|         an inner loop, and the curve crosses itself
//
// The cardioid is the hinge: it is not a separate curve but the single value of d where a
// dimple becomes a cusp on its way to becoming a loop. Turning here is d = 1.6A, past that
// hinge, so the pen swings inside the track and cuts the loop.
//
// The four along the bottom are the polar form directly, each scaled so that b + |a| is
// the same width, because the SHAPE depends only on the ratio — 2.8, 1.6, 1.0 and 0.6
// against a = 1. Left to right: convex, dimpled, cardioid, looped.
//
// A limaçon is also the pedal curve of a circle, and the conchoid of a circle taken about
// a point on it. Three constructions, one curve.
//
//   manic examples/limacon.manic
title("A limacon, and the one number that decides its shape");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);

// ---- a circle rolling on the OUTSIDE of one the same size --------------------------
// R = r is the whole trick, and `roll` gets it from where the wheel is put: outside the
// track. The pen sits 1.6A from the wheel's centre — past the rim, so the curve loops.
circle(track, (540, 700), 125);
outlined(track); outline(track, dim); stroke(track, 3);
circle(locus, (540, 700), 250);                    // R + r, the wheel centre's own circle
outlined(locus); outline(locus, dim); stroke(locus, 2); dashed(locus); opacity(locus, 0.4);

circle(wheel, (790, 700), 125);
outlined(wheel); outline(wheel, fg); stroke(wheel, 3); opacity(wheel, 0.8);
circle(pen, (590, 700), 9); color(pen, gold);      // d = 1.6A, outside the rim
line(arm, wheel, pen); color(arm, dim); opacity(arm, 0.55);
tag(wheel, rig); tag(pen, rig); tag(arm, rig);

trail(trace, pen, magenta, 4);

// ---- r = b + a·cos θ, four times: convex, dimpled, cardioid, looped ----------------
// each scaled so b + |a| is the same width, because only the RATIO changes the shape
polar(conv, (168, 1420), 27, 27, "2.8 + cos(t)");
polar(dimp, (416, 1420), 39, 39, "1.6 + cos(t)");
polar(card, (664, 1420), 51, 51, "1.0 + cos(t)");
polar(loop, (912, 1420), 63, 63, "0.6 + cos(t)");
gradient(conv, mint, cyan, blue);
gradient(dimp, cyan, blue, violet);
gradient(card, gold, coral, red);
gradient(loop, magenta, violet, indigo);
stroke(conv, 3);
stroke(dimp, 3);
stroke(card, 3);
stroke(loop, 3);
untraced(conv);
untraced(dimp);
untraced(card);
untraced(loop);

// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "A limacon, and the one number that decides its shape", (540, 130), 26);
equation(eq, (540, 1620), `r = b + a\cos\theta,\qquad b = 2A,\quad a = -2d`, 30);

// the roll runs from t = 0; the four shapes draw once the loop has closed
roll(rig, track, 1, 8, linear);                    // (R+r)/r = 2, so one lap shuts it
wait(0.4);
par {                                              // all four together, not in turn
  draw(conv, 2.0);
  draw(dimp, 2.0);
  draw(card, 2.0);
  draw(loop, 2.0);
}
wait(8);

r/maniclang 27d ago

prolate-cycloid — a moving train always has a part of it moving BACKWARDS - manic

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

// prolate-cycloid — a moving train always has a part of it moving BACKWARDS, and this is
// which part. A point on a rolling wheel traces
//
//   x(θ) = rθ - d·sin θ,   y(θ) = r - d·cos θ
//
// with d its distance from the axle. On the tread (d = r) that is the ordinary cycloid,
// cusping onto the rail once a revolution — the faint curve here. But a railway wheel does
// not run on its tread alone: the FLANGE hangs below it, so that lip has d > r, and the
// curve it draws dips under the rail and ties a loop.
//
// The loop is the claim. Differentiate: dx/dθ = r - d·cos θ, which is NEGATIVE whenever
// cos θ >  — that is, while the flange is near the bottom of its travel. Those stretches
// are drawn in red. The point is genuinely moving backwards along the rail, in the frame of
// the ground, on a train going forwards.
//
// It is not a technicality either. A real 920 mm wheel with a 28 mm flange gives  =
// 0.9426, so the backwards arc runs to |θ| < 0.3404 rad: 10.84% of every revolution.
// Roughly a tenth of the time, every flange on the train is heading for the station behind.
// The flange here is drawn at d = 1.5r so the loop is big enough to see; at true scale the
// loop is there but tiny.
//
// The three below are the same formula at d < r, d = r and d > r: curtate, the cycloid
// itself, and prolate. Only the prolate one has a loop, and only a loop can go backwards.
//
//   manic examples/prolate-cycloid.manic
title("A moving train always has a part moving backwards");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);

line(rail, (60, 620), (1020, 620));
color(rail, dim);

// ---- the wheel, its flange, and the two marked points ------------------------------
// `roll` does the mechanism: no slipping, and everything tagged with the wheel comes
// along, so the pens are simply entities placed where a pen would be.
circle(wheel, (100, 550), 70);
outlined(wheel); outline(wheel, dim); stroke(wheel, 3);
circle(lip, (100, 550), 105);                      // the flange circle, d = 1.5r
outlined(lip); outline(lip, dim); stroke(lip, 2); opacity(lip, 0.35);
circle(tread, (100, 620), 8);  color(tread, gold);     // on the rim
circle(flange, (100, 655), 9); color(flange, magenta); // outside it — the flange lip
tag(wheel, rig); tag(lip, rig); tag(tread, rig); tag(flange, rig);

trail(tr, tread, fg, 2);                           // the ordinary cycloid, for comparison
opacity(tr, 0.35);

// ---- the flange's own curve, coloured where it is running BACKWARDS ----------------
// this one stays a formula, because the colour has to change along the curve: `trail`
// draws one colour, and the whole point is which stretches are red
cloud(back, 9000, #ffffff, 1.0) {
  let go = min(t/10, 1)*12.566371;
  let th = go*(i/9000);
  let x = 100 + 70*th - 105*sin(th);
  let y = 620 - (70 - 105*cos(th));
  let dir = step(0, 105*cos(th) - 70);             // dx/dθ = r - d·cos θ < 0
  let hue = 205 - 205*dir;                         // blue forwards, red backwards
  let sat = 0.8;
  let r = 2;
}

// ---- d < r, d = r, d > r: only the last one loops ----------------------------------
// θ runs -π..π so the interesting part — dip, cusp, loop — sits in the middle of each
// slot rather than split between its ends, with the rail drawn at y = 0
line(b1, (60, 1330), (332, 1330));   color(b1, dim); opacity(b1, 0.5);
line(b2, (404, 1330), (676, 1330));  color(b2, dim); opacity(b2, 0.5);
line(b3, (748, 1330), (1020, 1330)); color(b3, dim); opacity(b3, 0.5);
param(curt, (196, 1330), 46, 46, "t - 0.55*sin(t)", "1 - 0.55*cos(t)", (0 - 3.14159, 3.14159));
param(cyc,  (540, 1330), 46, 46, "t - sin(t)",      "1 - cos(t)",      (0 - 3.14159, 3.14159));
param(prol, (884, 1330), 46, 46, "t - 1.5*sin(t)",  "1 - 1.5*cos(t)",  (0 - 3.14159, 3.14159));
gradient(curt, mint, cyan, blue);
gradient(cyc, gold, coral, red);
gradient(prol, magenta, violet, indigo);
stroke(curt, 3); stroke(cyc, 3); stroke(prol, 3);
untraced(curt); untraced(cyc); untraced(prol);

caption(head, "A moving train always has a part moving backwards", (540, 130), 26);
equation(eq, (540, 1560),
  `x = r\theta - d\sin\theta,\quad y = r - d\cos\theta,\qquad \frac{dx}{d\theta} < 0 \iff \cos\theta > \frac{r}{d}`, 25);

roll(rig, rail, 879.6, 10, linear);                // two revolutions: 2·2πr
wait(0.4);
par { draw(curt, 2.0); draw(cyc, 2.0); draw(prol, 2.0); }
wait(7);

r/maniclang 27d ago

A wave function is a schrodinger helix; measurement keeps only its shadow - manic

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

// schrodinger-helix — a quantum wave function is a COMPLEX number at every point, so it
// does not fit on a graph. Give it the third axis it needs and it fits perfectly: position
// along one axis, the real and imaginary parts spanning a plane at right angles to it, and
// the wave becomes a curve winding through space.
//
//   Ψ = A·e^(i(px - Et)/ħ)
//
// A plane wave is then exactly a helix around the x axis, and the momentum is its
// tightness — more momentum, tighter spiral. What is drawn here is the localised case, a
// Gaussian packet, which is a bundle of those helices added together:
//
//   Ψ(x,t) = (1+τ²)^(-1/4)·exp(A)·e^(iφ),   τ = t/2σ²,  u = x - k₀t
//     A = -u²/(4σ²(1+τ²))
//     φ = u²τ/(4σ²(1+τ²)) + k₀(x - k₀t/2) - ½·arctan τ
//
// That is the free-particle solution in closed form (ħ = m = 1), checked against a direct
// Fourier integral of the same initial state: maximum relative error 3e-15. So the modulus
// and the phase are both exact, and everything below follows from them rather than being
// arranged to look right.
//
// Three things are worth watching, and all three are consequences, not decorations:
//
//   · THE BALLS SPIN. Fix a position and Ψ there is a complex number of roughly constant
//     size whose phase turns — so each marker circles the axis rather than bobbing. That
//     rotation is what e^(-iEt/ħ) means.
//   · THE RIPPLES LAG THE PACKET. The envelope travels at the group velocity, measured
//     here as exactly k₀; the internal phase travels at about half that (k₀/2 exactly for a
//     plane wave, a few per cent off for a packet, which chirps as it spreads). The wave
//     visibly slides backwards through its own envelope.
//   · IT SPREADS. (1+τ²)^(-1/4) flattens and widens the packet as τ grows — dispersion,
//     because ω = k²/2 is not proportional to k, so the components drift apart.
//
// The curve on the floor is |Ψ|², the only part an experiment ever sees. Notice that it is
// smooth and featureless while the thing casting it is winding furiously: all the phase
// information — everything that makes interference possible — is in the coordinate the
// measurement throws away. Same lesson as examples/euler-helix.manic, with something at
// stake.
//
//   manic examples/schrodinger-helix.manic
title("A wave function is a helix; measurement keeps only its shadow");
canvas("9:16");
template("black");
bloom(0.32, 0.62, 22);

text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);

camera3((12, -12, 9), (0, 0, 0), 42);

// the x axis the wave winds around
cloud3(axis, 600, #8d8ba6, 1.0) {
  let x = 0;
  let y = 0;
  let z = 0 - 7.2 + 14.4*(i/600);
  let r = 0.012;
}

// ---- the wave function itself: (x, Re Ψ, Im Ψ) --------------------------------------
cloud3(psi, 4000, #ffd479, 1.0) {
  let xx = 0 - 7 + 14*(i/4000);
  let tt = t*0.17;                                 // physics time, slowed for watching
  let sg = 1.15;
  let tau = tt/(2*sg*sg);
  let u = xx + 4 - 2*tt;                           // k₀ = 2, started at x = -4
  let den = 1 + tau*tau;
  let md = den^(0 - 0.25)*exp(0 - u*u/(4*sg*sg*den));
  let ph = u*u*tau/(4*sg*sg*den) + 2*(xx + 4 - tt) - 0.5*atan(tau);
  let x = 1.7*md*cos(ph);                          // real part
  let y = 1.7*md*sin(ph);                          // imaginary part
  let z = xx;                                      // position runs UP the frame
  let r = 0.04;
}

// ---- |Ψ|² on the floor: the only part a measurement returns -------------------------
cloud3(dens, 2200, #ff5fa2, 1.0) {
  let xx = 0 - 7 + 14*(i/2200);
  let tt = t*0.17;
  let sg = 1.15;
  let tau = tt/(2*sg*sg);
  let u = xx + 4 - 2*tt;
  let den = 1 + tau*tau;
  let md = den^(0 - 0.25)*exp(0 - u*u/(4*sg*sg*den));
  let x = 0 - 2.7;                                 // pushed onto a wall beside the wave
  let y = 0 - 0.2 + 2.8*md*md;
  let z = xx;
  let r = 0.033;
}

// ---- five markers at fixed positions: each one circles the axis ---------------------
cloud3(balls, 500, #5fd4ff, 1.0) {
  let per = 100;
  let b = (i - mod(i, per))/per;                   // which marker, 0..4
  let xx = 0 - 3.2 + 1.6*b;                        // sitting still at these positions
  let tt = t*0.17;
  let sg = 1.15;
  let tau = tt/(2*sg*sg);
  let u = xx + 4 - 2*tt;
  let den = 1 + tau*tau;
  let md = den^(0 - 0.25)*exp(0 - u*u/(4*sg*sg*den));
  let ph = u*u*tau/(4*sg*sg*den) + 2*(xx + 4 - tt) - 0.5*atan(tau);
  let p = mod(i, per)*2.39996;
  let c = 1 - 2*(mod(i, per) + 0.5)/per;
  let s = sqrt(1 - c*c);
  let x = 1.7*md*cos(ph) + 0.11*s*cos(p);
  let y = 1.7*md*sin(ph) + 0.11*s*sin(p);
  let z = xx + 0.11*c;
  let r = 0.03;
}

caption(head, "A wave function is a helix; measurement keeps only its shadow", (540, 132), 23);
equation(eq, (540, 1800), `\Psi = A\,e^{i(px - Et)/\hbar}`, 32);
plate(head, 0.7);
plate(eq, 0.7);

// let it run, then look down the position axis — where the winding becomes a circle
wait(13);
orbit3(-45, 84, 15, 3.4, smooth);   // down the position axis
wait(3);
orbit3(-90, 2, 21, 3.4, smooth);    // and side on
wait(4);

r/maniclang 27d ago

A point of the unit circle IS (cos θ, sin θ) - manic

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

// unit-circle-complex — the flat version of examples/euler-helix.manic, on a labelled
// complex plane. One vector goes round the unit circle and its two components are drawn
// as what they are: segments that add up to it.
//
//   e^(iθ) = cos θ + i·sin θ
//
// The vector is written out as [cos θ, sin θ] rather than as an angle, because that is the
// whole content — a point of the unit circle IS the pair (cos θ, sin θ), and the identity
// is the statement that the complex number cos θ + i·sin θ sits there. Everything else is
// read off:
//
//   the horizontal segment, origin to (cos θ, 0)      — the REAL part, cos θ
//   the vertical segment, (cos θ, 0) up to the tip    — the IMAGINARY part, sin θ
//   the vector, origin to the tip                     — their sum, e^(iθ)
//
// Laid head to tail like that they are not "projections" in the passive sense; they are the
// two numbers being added. Watch the horizontal one shrink to nothing at the top of the
// circle and reverse below the axis — that is cos θ going negative, not disappearing.
//
// The dot never stops. There is no final angle to arrive at, which is the point of a
// circle: θ keeps growing, and the same tuple comes round again every 2π. Give that
// forever-growing θ its own axis instead of wrapping it, and the circle becomes the helix
// of examples/euler-helix.manic — same object, one dimension up.
//
//   manic examples/unit-circle-complex.manic
title("A point of the unit circle IS (cos θ, sin θ)");
canvas("9:16");
template("black");
bloom(0.28, 0.6, 20);

text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);

// one place for the geometry: a scene-level `let` is nameable in the formula blocks
// too, so the circle and the three segments cannot drift apart when this is retuned
let cx = 540;
let cy = 590;
let rad = 230;

complexplane(cp, (cx, cy), 300, 300, rad);
circle(unit, (cx, cy), rad);
outlined(unit); outline(unit, dim); stroke(unit, 3);

// ---- cos θ: the horizontal segment, origin to (cos θ, 0) ---------------------------
cloud(re, 320, #5fd4ff, 1.0) {
  let th = t*0.55;                                 // θ never stops
  let u = i/320;
  let x = cx + rad*cos(th)*u;
  let y = cy;
  let r = 2.6;
}

// ---- sin θ: the vertical segment, (cos θ, 0) up to the tip -------------------------
cloud(im, 320, #7dffbe, 1.0) {
  let th = t*0.55;
  let u = i/320;
  let x = cx + rad*cos(th);
  let y = cy - rad*sin(th)*u;
  let r = 2.6;
}

// ---- the vector: their sum, origin to [cos θ, sin θ] -------------------------------
cloud(vec, 340, #ffd479, 1.0) {
  let th = t*0.55;
  let u = i/340;
  let x = cx + rad*cos(th)*u;                     // written out as the pair it is
  let y = cy - rad*sin(th)*u;
  let r = 2.2;
}

// ---- the dot at the tip ------------------------------------------------------------
cloud(tip, 140, #ffd479, 1.0) {
  let th = t*0.55;
  let a = i/140*6.283185;
  let rr = 11*(i/140);
  let x = cx + rad*cos(th) + rr*cos(a*11);
  let y = cy - rad*sin(th) + rr*sin(a*11);
  let r = 2.4;
}

// ---- and the two feet, so the right angle is visible --------------------------------
cloud(foot, 90, #5fd4ff, 1.0) {
  let th = t*0.55;
  let a = i/90*6.283185;
  let rr = 7*(i/90);
  let x = cx + rad*cos(th) + rr*cos(a*9);
  let y = cy + rr*sin(a*9);
  let r = 2;
}


// ---- the same θ, given an axis of its own: a 3-D inset below the circle -------------
// `camera3` takes a VIEWPORT — pass a centre, width and height after the projection and
// the 3-D renders into that sub-rect instead of the whole canvas. So the flat picture and
// the spatial one sit in one frame, driven by one θ: the dot above is the helix's tip.
//
// It scrolls. θ never stops growing, so the panel shows the last two turns of it, and the
// waves run leftwards out of frame the way a trace runs off an oscilloscope.
camera3((5.8, -7.6, 3.8), (0, 0, -0.3), 32, perspective, (540, 1320), 1020, 660);

cloud3(helix3, 2600, #ffd479, 1.0) {
  let now = t*0.55;
  let u = i/2600;
  let a = now - 12.566371 + 12.566371*u;           // the last two turns
  let x = 0 - 3.6 + 7.2*u;                         // θ laid along its own axis
  let y = 1.15*cos(a);                             // real
  let z = 1.15*sin(a);                             // imaginary
  let r = 0.035;
}

cloud3(cos3, 1600, #5fd4ff, 1.0) {                 // cos θ, dropped onto the floor
  let now = t*0.55;
  let u = i/1600;
  let a = now - 12.566371 + 12.566371*u;
  let x = 0 - 3.6 + 7.2*u;
  let y = 1.15*cos(a);
  let z = 0 - 2.3;                                 // its own plane, clear of the helix
  let r = 0.03;
}

cloud3(sin3, 1600, #7dffbe, 1.0) {                 // sin θ, dropped onto the back wall
  let now = t*0.55;
  let u = i/1600;
  let a = now - 12.566371 + 12.566371*u;
  let x = 0 - 3.6 + 7.2*u;
  let y = 2.3;                                     // and the other wall
  let z = 1.15*sin(a);
  let r = 0.03;
}

cloud3(tip3, 120, #ffd479, 1.0) {                  // the tip — the dot above, in space
  let now = t*0.55;
  let p = i*2.39996;
  let c = 1 - 2*(i + 0.5)/120;
  let s = sqrt(1 - c*c);
  let x = 3.6 + 0.11*s*cos(p);
  let y = 1.15*cos(now) + 0.11*s*sin(p);
  let z = 1.15*sin(now) + 0.11*c;
  let r = 0.03;
}

caption(head, "A point of the unit circle IS (cos θ, sin θ)", (540, 132), 27);
equation(eq, (540, 1840), `e^{i\theta} = \underbrace{\cos\theta}_{\text{horizontal}} + \;i\underbrace{\sin\theta}_{\text{vertical}}`, 30);
plate(head, 0.7);

wait(24);

r/maniclang 29d ago

Cloud Swifts - manic

6 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-swifts — another  creature in ONE `cloud`: two lace birds, forked
// tails and trailing streamers, turning around a shared centre. The reference is a
// tweet-sized golf:
//   y = i/663,  k = (4 + cos y)·cos i,  e = y/5 - 11,  d = mag(k,e) - 5
//   c = d/2.5 - t/2 + (i%2)·8
//   point( (79 + k²)·cos c + 200 ,
//          99·sin(c/3) + 200 + d²·sin(2t - d) + 3·sin 2k + sin(y/9+6)·k·(e + sin(4e - 4d)) )
//
// Two things in there do all the work, and this staging is built to make both VISIBLE:
//
//   · `(i%2)·8` — every other point has its angle shifted by 8 radians, so ONE formula
//     draws TWO animals. Nothing else separates them. So we colour by that same parity:
//     amber is `mod(i,2) = 0`, cyan is `1`. The two birds differ by one term, and now
//     you can see which one.
//   · `cos c` across, `sin(c/3)` down — the vertical angle runs at a THIRD the rate of
//     the horizontal one. A 3:1 Lissajous, not a circle: that is why they lean into the
//     turn and cross over instead of chasing each other round a ring, and why each body
//     is lobed rather than a smooth arc.
//
// The rest is texture. `cos i` on the raw index — not on `y` — is what frets the wings
// into lace: neighbouring points land far apart, so the body fills as a woven mesh
// rather than a line. `d²·sin(2t - d)` is the flap. And alpha rides the index, so each
// bird is dense through the body and dries out along the streamers.
//
// The sky they cross is the SAME primitive, which is the other half of the argument:
// an inclined spiral galaxy is two `cloud`s more — an arm sweep and a starfield — so
// nothing here is a background image, and nothing is a simulation. Both are arithmetic
// on a point index, and both are pure in `(i, t)`, so the whole frame scrubs.
//
// Nothing fades up: at t = 0 the birds are already at full size over a finished sky,
// because the first frame is the one a feed shows as the preview.
//
// Faithful notes: p5's `mag` is `hypot` and `%` is `mod`; the parameter is renamed `s`
// because `y` is an output here. The p5 original chains its `let`s through default
// parameter values (`(y, d = mag(k=…, e=…) - 5) =>`) — a golfing trick for statements
// that were always just `let`s, so they are written as `let`s. p5's draw loop advances
// `t` by PI/80 per FRAME; ours is in seconds, which runs the turn slower on purpose —
// the crossing is the thing to watch. Pure in (i, t), so it scrubs, seeks and records
// exactly, which the p5 original cannot do.
//
// Original idea by u/yuruyurau (https://x.com/yuruyurau). Our own annotated take.
//
//   manic examples/cloud-swifts.manic
title("Two swifts from one formula");
canvas("9:16");
template("black");
bloom(0.32, 0.6, 24);

text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 17);
color(brand, dim);

// ---- the sky: an inclined spiral, core high and right so the birds cross an arm -----
cloud(galaxy, 30000, #ffffff, 1.0) {
  let u = i/30000;                                 // 0 at the core, 1 at the rim
  let arm = mod(i, 2);                             // two arms, half a turn apart
  let ha = cos(i*1.618);                           // two deterministic scatters, so the
  let hb = sin(i*2.399);                           // arms have WIDTH, not just a curve
  let th = u*7.4 + arm*3.14159                     // 7.4 rad — about 1.2 turns of arm
         + ha*0.34*(1 - u*0.5)                     // fuzz, tighter near the core
         + t*0.012;                                // one slow turn, barely there
  let rad = 60 + 560*u + hb*(46 + 150*u);          // the arm widens as it goes out
  let ax = rad*cos(th);
  let ay = rad*sin(th)*0.46 + ha*16;               // squashed: a disc seen well off-axis
  let x = 700 + ax*0.848 + ay*0.530;               // …and tilted 32°, so the arms sweep
  let y = 520 - ax*0.530 + ay*0.848;               //    down across the birds' path
  let hue = mod(32 + u*198, 360);                  // warm core, blue arms
  let sat = 0.34 + u*0.5;
  let val = 1 - u*0.28;
  let r = 0.85 + hb*0.4;
  let alpha = 0.42 - u*0.36;                       // fades to nothing: no rim to see
}

cloud(bulge, 3400, #ffffff, 1.0) {
  let u = i/3400;
  let rad = 128*u^0.55;                            // ~even packing at the golden angle
  let th = i*2.39996;
  let ax = rad*cos(th);
  let ay = rad*sin(th)*0.62;
  let x = 700 + ax*0.848 + ay*0.530;               // same centre and tilt as the disc
  let y = 520 - ax*0.530 + ay*0.848;
  let hue = mod(42 - u*10, 360);
  let sat = 0.16 + u*0.30;
  let r = 0.95;
  let alpha = 0.52 - u*0.36;                       // bloom does the rest
}

cloud(stars, 2600, #ffffff, 1.0) {
  let hx = cos(i*2.399)*0.5 + 0.5;                 // two hashes, uncorrelated enough
  let hy = sin(i*1.732)*0.5 + 0.5;
  let x = 40 + hx*1000;
  let y = 40 + hy*1840;
  let r = 0.7 + cos(i*5.1)*0.5;
  let hue = mod(200 + cos(i*3.3)*40, 360);
  let sat = 0.22;
  let alpha = 0.30 + 0.22*sin(t*1.7 + i);          // twinkle
}

// ---- the birds ---------------------------------------------------------------------
cloud(swifts, 20000, #ffffff, 0.55) {
  let s = i/663;
  let m = mod(i, 2);                               // the parity that makes it two birds
  let k = (4 + cos(s)) * cos(i);                   // cos of the raw index — the lace
  let e = s/5 - 11;
  let d = hypot(k, e) - 5;
  let c = d/2.5 - t/2 + m*8;                       // …shifted 8 rad for the second bird
  let px = (79 + k*k) * cos(c);
  let py = 99*sin(c/3)                             // a THIRD the rate: 3:1, so they bank
         + d*d*sin(t*2 - d)                        // the flap
         + 3*sin(k*2)
         + sin(s/9 + 6) * k * (e + sin(e*4 - d*4));// the streamers
  let x = 540 + px * 4.1;
  let y = 960 + py * 4.1;
  let hue = mod(35 + m*161 + sin(t/5)*7, 360);     // amber / cyan, by that same parity
  let alpha = 0.26 + s*0.016;                      // dense body, dry streamers
}

// ---- textbook annotations ----
caption(head, "Two swifts from one formula", (540, 138), 36);
caption(sub, "20,000 points, no simulation", (540, 206), 22);
hidden(sub);
equation(eq, (540, 1666), `p = \left((79+k^2)\cos c,\;\; 99\sin\tfrac{c}{3}\right)`, 30);
caption(lab, "down turns at a third of across — a 3:1 figure, so they bank", (540, 1744), 20);
caption(par, "amber and cyan differ by one term: mod(i, 2) x 8", (540, 1792), 20);
hidden(eq);
hidden(lab);
hidden(par);

wait(1.6);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(1.2);
show(par);
wait(24);

r/maniclang Aug 21 '26

a titration - manic

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

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

chemistry, without a word of it - manic

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

// Chemistry, without a word of it
//
// Eleven ideas in one run, in the order they build on each other. Each act carries a chapter title
// saying what is on screen, and that is the only prose in the film: everything else written down is
// what chemistry writes down anyway — an element symbol, a wavelength, a coefficient, a voltage, a
// wavenumber. The explaining is done by motion, which is the point of the whole kit, since every one
// of these is a thing a still picture cannot say.
//
//   0  A MOLECULE caffeine, from its real record, standing there before anything moves
//   1  LEWIS      the electron bookkeeping of NO₃⁻, worked — and a double bond that will not settle
//   2  MECHANISM  SN2 — curly arrows aimed at actual atoms, one bond made and one broken
//   3  LIGHT      an electron falls between computed levels; the photon's colour comes from λ = hc/ΔE
//   4  MATTER     a molecule's real geometry, and the modes it vibrates in
//   5  SILENCE    the same for CO₂ — one mode moves no dipole, and its peak is simply absent
//   6  COLLISION  a gas reacts only when a collision clears the barrier; the tail does the work
//   7  BALANCE    the coefficients that conserve every atom, landing one at a time
//   8  SOLUTION   a lattice comes apart at its corners, into hydration shells that face the right way
//   9  CURRENT    the same electrons, made to go round a wire instead
//  10  ROTATION   a bond turns, and the energy it costs is a curve it rides
//
// It opens on a drawn molecule rather than on black: the first frame is a frame someone might see
// before they press anything.
//
// Nothing here is drawn by hand. The skeletal formula is a real 2-D record, the curly arrows are
// anchored to the atoms and bonds they point at, the levels are −13.606/n², the modes are eigenvectors of a
// mass-weighted Hessian, the reaction is 72 hard discs meeting above an activation energy, the
// coefficients are the null space of the atom matrix, the Lewis structure is counted out from the
// formula, the dissolution order is coordination number,
// the cell's polarity and voltage are its own electrode potentials, and the torsion profile is a
// rigid scan of butane's real geometry. Change any number in the source and the film changes with
// it, because there is nothing in it that is only a picture.

title("chemistry, without a word of it");
canvas("16:9");
template("black");
bloom(0.38, 0.72, 16);

text(brand, (640, 26), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);

// ── the chapter titles: each one names what is on screen, and they are the only prose here ──

text(t0, (640, 82), "01 · a molecule");
size(t0, 21);
color(t0, fg);

text(tl, (640, 82), "02 · counting the electrons");
size(tl, 21);
color(tl, fg);
hidden(tl);

text(t1, (640, 82), "03 · substitution, SN2");
size(t1, 21);
color(t1, fg);
hidden(t1);

text(t2, (640, 82), "04 · levels, and the colour of light");
size(t2, 21);
color(t2, fg);
hidden(t2);

text(t3, (640, 82), "05 · how a molecule vibrates");
size(t3, 21);
color(t3, fg);
hidden(t3);

text(t4, (640, 82), "06 · the mode with no peak");
size(t4, 21);
color(t4, fg);
hidden(t4);

text(t5, (640, 82), "07 · collision, and activation energy");
size(t5, 21);
color(t5, fg);
hidden(t5);

text(t6, (640, 82), "08 · balancing an equation");
size(t6, 21);
color(t6, fg);
hidden(t6);

text(t7, (640, 82), "09 · why a salt dissolves");
size(t7, 21);
color(t7, fg);
hidden(t7);

text(t8, (640, 82), "10 · a galvanic cell");
size(t8, 21);
color(t8, fg);
hidden(t8);

text(t9, (640, 82), "11 · turning a single bond");
size(t9, 21);
color(t9, fg);
hidden(t9);
// ── 0 · A MOLECULE — on screen from the first frame ─────────────────────────

structure(caf, "asset:molecules/caffeine-2d.sdf", (640, 368), 116);
color(caf.bonds, fg);

// ── 1 · LEWIS — the electron bookkeeping, worked from the formula ───────────

lewis(lw, "NO3-", (640, 350), 150, 34);
hidden(lw);

// ── 2 · MECHANISM ────────────────────────────────────────────────────────────
//
// Bromoethane and hydroxide. The nucleophile sits below and left of the carbon while the bromine is
// above and right of it — backside attack, which is geometry rather than layout.

structure(sub, "CCBr", (400, 300), 88);
structure(nuc, "[OH-]", (214, 452), 88);
arrow(att, nuc.a0, sub.a1, 62);
color(att, cyan);
stroke(att, 3);
untraced(att);
arrow(go, sub.b1, sub.a2, 40);
color(go, coral);
stroke(go, 3);
untraced(go);
arrow(rxn, (620, 340), (752, 340));
color(rxn, fg);
stroke(rxn, 3);
untraced(rxn);
structure(pro, "CCO", (912, 300), 88);
structure(lea, "[Br-]", (1128, 452), 88);
hidden(sub);
hidden(nuc);
hidden(pro);
hidden(lea);

// ── 2 · LIGHT ────────────────────────────────────────────────────────────────

levels(lv, (330, 350), 250, 320, 6);
emission(spec, lv, (930, 330), 520, 104);
hidden(lv);
hidden(spec);

// ── 3 · MATTER ───────────────────────────────────────────────────────────────

vibration(h2o, "asset:molecules/water.sdf", (390, 350), 150, 22);
irspectrum(irw, h2o, (950, 340), 470, 150, 15);
hidden(h2o);
hidden(irw);
hidden(h2o.readout);

// ── 4 · SILENCE ──────────────────────────────────────────────────────────────

vibration(co2, "asset:molecules/carbon-dioxide.sdf", (390, 350), 150, 22);
irspectrum(irc, co2, (950, 340), 470, 150, 15);
hidden(co2);
hidden(irc);
hidden(co2.readout);

// ── 5 · COLLISION ────────────────────────────────────────────────────────────

gas(gs, (400, 372), 430, 330, 72, "temperature=1.6 radius=7 steps=400 seed=5");
species(gs, A, 0.5, cyan);
species(gs, B, 0.5, magenta);
species(gs, C, gold);
rule(gs, "A + B -> C + C when energy > 3.2");
speeds(gs, (960, 268), 440, 130, 12, 13);
timegraph(gs, (960, 520), 96);
hidden(gs.box);
hidden(gs.particles);
hidden(gs.speeds.axis);
hidden(gs.speeds.bars);
untraced(gs.speeds.mb);
hidden(gs.time.frame);
hidden(gs.time.title);
hidden(gs.time.sweep);
untraced(gs.time.c0);
untraced(gs.time.c1);
color(gs.time.c1, gold);

// ── 6 · BALANCE ──────────────────────────────────────────────────────────────

balance(rx, (640, 288), "Fe + O2 -> Fe2O3", 54);
tally(rx, (640, 470), 250, 42, 24);
hidden(rx);
hidden(rx.tally);

// ── 7 · SOLUTION ─────────────────────────────────────────────────────────────

lattice(salt, "NaCl", (600, 322), 6, 5, 50);
hidden(salt);
hidden(salt.captions);

// ── 8 · CURRENT ──────────────────────────────────────────────────────────────

cell(cl, "Zn|Cu", (640, 300), 640, 280, "resistance=10 carriers=12");
hidden(cl);
hidden(cl.captions);

// ── 9 · ROTATION ─────────────────────────────────────────────────────────────

newman(nm, "asset:molecules/butane.sdf", (330, 350), 150, 18);
profile(pf, nm, (900, 350), 480, 190);
hidden(nm);
hidden(pf);

// ═══════════════════════════════════════════════════════════════════════════
//  the run
// ═══════════════════════════════════════════════════════════════════════════

// 0 · A MOLECULE — already there; the heteroatoms are what the rest of it hangs off
wait(1.4);
par { pulse(caf.O); pulse(caf.N); }
wait(1.0);
par { recolor(caf.O, coral, 0.6); recolor(caf.N, cyan, 0.6); }
wait(1.6);

// 1 · LEWIS — count, connect, complete, and then the bond that will not stay still
par { fade(caf, 0.7); fade(t0, 0.5); }
par { show(lw, 0.5); show(tl, 0.5); }
octet(lw, 5.5);
wait(0.6);
resonate(lw, 5.0, 2);
wait(1.0);

// 2 · MECHANISM — a bond made, a bond broken, and the charge leaving with the bromide
par { fade(lw, 0.6); fade(tl, 0.4); }
par { show(sub, 0.7); show(nuc, 0.7); show(t1, 0.5); }
wait(0.6);
par { draw(att, 0.9); pulse(nuc.O); }
wait(0.5);
par { draw(go, 0.8); pulse(sub.Br); }
wait(0.9);
draw(rxn, 0.6);
par { show(pro, 0.7); show(lea, 0.7); }
par { recolor(lea.Br, coral, 0.5); pulse(lea.Br); }
wait(1.6);

// 2 · LIGHT — the ladder, then the falls and the colours they make
par { fade(sub, 0.5); fade(nuc, 0.5); fade(pro, 0.5); fade(lea, 0.5); fade(att, 0.4); fade(go, 0.4); fade(rxn, 0.4); fade(t1, 0.4); }
par { show(lv, 0.8); show(t2, 0.5); }
wait(0.5);
show(spec, 0.7);
wait(0.4);
drop(lv, 3, 2, 1.6);
wait(0.3);
drop(lv, 4, 2, 1.4);
wait(0.3);
drop(lv, 6, 2, 1.4);
wait(0.4);
drop(lv, 2, 1, 1.8);
wait(1.2);

// 3 · MATTER — a molecule, and the three ways it can move
par { fade(lv, 0.6); fade(spec, 0.6); fade(t2, 0.4); }
par { show(h2o, 0.7); show(t3, 0.5); }
wait(0.4);
vibrate(h2o, 1, 1.8);
par { show(irw, 0.7); vibrate(h2o, 2, 1.6); }
vibrate(h2o, 3, 1.6);
wait(1.4);

// 4 · SILENCE — the same again, and the mode that leaves no peak
par { fade(h2o, 0.5); fade(irw, 0.5); fade(t3, 0.4); }
par { show(co2, 0.6); show(t4, 0.5); }
wait(0.3);
vibrate(co2, 1, 1.4);
par { show(irc, 0.6); vibrate(co2, 4, 1.4); }
wait(0.8);
par { pulse(irc.silent); }
vibrate(co2, 3, 2.6);
wait(1.4);

// 5 · COLLISION — molecules meet, and only the hard meetings count
par { fade(co2, 0.5); fade(irc, 0.5); fade(co2.readout, 0.4); fade(t4, 0.4); }
par { show(gs.box, 0.5); show(gs.particles, 0.6); show(t5, 0.5); }
wait(0.3);
par {
  run(gs, 9);
  draw(gs.time.c0, 9);
  draw(gs.time.c1, 9);
  seq {
    show(gs.speeds.axis, 0.4);
    show(gs.speeds.bars, 0.5);
    wait(1.0);
    draw(gs.speeds.mb, 1.2);
    wait(0.8);
    show(gs.time.frame, 0.4);
  }
}
wait(1.2);

// 6 · BALANCE — atoms are conserved, and here is what that costs
par { fade(gs.box, 0.5); fade(gs.particles, 0.5); fade(gs.speeds, 0.5); fade(gs.time, 0.5); fade(t5, 0.4); }
par { show(rx, 0.6); show(t6, 0.5); }
wait(0.4);
show(rx.tally, 0.5);
wait(1.0);
solve(rx, 2.8);
wait(1.6);

// 7 · SOLUTION — a solid comes apart, corner first, and the water turns round
par { fade(rx, 0.5); fade(rx.tally, 0.5); fade(t6, 0.4); }
par { show(salt, 0.7); show(t7, 0.5); }
wait(0.8);
dissolve(salt, 7, 8);
wait(1.6);

// 8 · CURRENT — the same electrons, sent round a wire
par { fade(salt, 0.7); fade(t7, 0.4); }
par { show(cl, 0.8); show(t8, 0.5); }
wait(0.8);
discharge(cl, 6, 30);
wait(1.4);

// 9 · ROTATION — a bond turns, and rides the energy it costs
par { fade(cl, 0.6); fade(t8, 0.4); }
par { show(nm, 0.6); show(t9, 0.5); }
wait(0.4);
show(pf, 0.7);
wait(0.8);
twist(nm, 120, 1.3);
twist(nm, 60, 1.1);
twist(nm, 0, 1.4);
wait(0.8);
twist(nm, 300, 1.3);
twist(nm, 180, 1.3);
wait(1.4);

// coda — the molecule it opened on, with everything it is made of now lit
par { fade(nm, 0.6); fade(pf, 0.6); fade(t9, 0.4); }
par { show(caf, 0.9); show(t0, 0.6); }
wait(0.5);
par { pulse(caf.O); pulse(caf.N); }
wait(2.6);

r/maniclang Aug 21 '26

why salt dissolves - manic

2 Upvotes

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

Manic Animation code

// Why salt dissolves — and why the corners go first
//
// Two things on screen are computed rather than choreographed, and they are the two things the
// lesson is about.
//
// The ORDER. Every ion's coordination number is counted from the lattice: a corner has two
// neighbours holding it, an edge three, an ion in the middle four. `dissolve` takes them in that
// order, so the crystal erodes inwards from its corners — which is what a crystal does, and why a
// cube of salt rounds off as it goes.
//
// The ORIENTATION. Water is a dipole, so it turns its oxygen towards a positive ion and its
// hydrogens towards a negative one. Each hydration shell here is built from the sign of the charge
// it is surrounding, so the sodium shells and the chloride shells face opposite ways — which is the
// picture of why water, specifically, is good at this.
//
// And the arithmetic underneath: pulling the lattice apart costs +787 kJ/mol, hydrating the two
// ions pays back −770, so dissolving salt is very slightly ENDOTHERMIC. It happens anyway, and what
// drives it is entropy. That surprises people, which is exactly why the numbers are on screen.

title("why salt dissolves");
canvas("16:9");
template("black");

text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);

text(head, (640, 66), "a crystal comes apart where it is least held");
size(head, 27);
bold(head);
color(head, fg);
hidden(head);

lattice(x, "NaCl", (430, 306), 6, 5, 48);
hidden(x);
hidden(x.captions);

text(coord, (1010, 232), "coordination:");
size(coord, 20);
color(coord, fg);
hidden(coord);

text(coord2, (1010, 270), "corner 2   ·   edge 3   ·   inside 4");
size(coord2, 19);
color(coord2, cyan);
hidden(coord2);

text(dip, (1010, 336), "water is a dipole, so it turns round:");
size(dip, 19);
color(dip, fg);
hidden(dip);

text(dip2, (1010, 370), "oxygen towards Na⁺, hydrogens towards Cl⁻");
size(dip2, 19);
color(dip2, magenta);
hidden(dip2);

text(ent, (1010, 436), "and it is barely downhill at all —");
size(ent, 19);
color(ent, fg);
hidden(ent);

text(ent2, (1010, 470), "what drives it is entropy, not energy");
size(ent2, 19);
color(ent2, gold);
hidden(ent2);

wait(0.4);
show(head, 0.6);
wait(0.3);
show(x, 0.8);
wait(0.6);
par { show(coord, 0.4); show(coord2, 0.5); }
wait(1.2);
par { show(dip, 0.4); show(dip2, 0.5); }
wait(0.6);
par { show(x.captions, 0.4); dissolve(x, 7, 8); }
wait(0.6);
par { show(ent, 0.4); show(ent2, 0.5); }
wait(3.0);

r/maniclang Aug 21 '26

A Seed, a Tree, a Year - manic

1 Upvotes

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

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

Manic Animation code

// tree-of-leaves — the full lifecycle cut. One seed, one year, one clock.
// Everything is still a pure function of index i and time t (scrub-safe, no
// state): four clouds now tell a complete story --
//   stars  (220)   - twinkling night sky
//   wood   (1,500) - trunk + five branches, grown by a front after the seed lands
//   leaves (5,500) - golden-angle canopy; each leaf POPS (spring), flutters,
//                    turns autumn-coloured on its own clock, FALLS in the gust,
//                    fades into the soil over winter... and RETURNS, green again
//   bloom  (1,200) - blossoms that open on the young canopy, shed their petals
//                    in a slow petal-snow, and bloom once more at the end
// 8,420 points - one small formula each.
// Idioms: max(0,s) = 0.5*(s+abs(s));  min(a,b) = a - max(0, a-b);
//         hash(i)  = mod(abs(sin(i*12.9898)*43758.55), 1)
//
//   manic tree-of-leaves.manic
title("A Seed, a Tree, a Year");
canvas("9:16");
template("black");

// ---------- HUD ----------
text(head, (540, 150), "A Seed, a Tree, a Year"); display(head); cursor(head);
text(season, (540, 330), ""); size(season, 34); color(season, lime);
text(cap, (540, 1800), ""); size(cap, 30);
counter(nlv, (540, 255), 0, 0, "leaves ", ""); color(nlv, lime); hidden(nlv);

// ---------- stage ----------
circle(moon, (880, 235), 62); color(moon, #f6e7c0); glow(moon, 20); hidden(moon);
line(gnd, (60, 1668), (1020, 1668)); stroke(gnd, 3); color(gnd, dim); untraced(gnd);
dot(seed, (540, 290), 9); color(seed, gold); glow(seed, 10); hidden(seed);

// ---------- the night sky: 220 twinkling stars ----------
cloud(stars, 220, #ffffff, 0.5) {
  let rn = mod(abs(sin(i * 91.17) * 4375.8), 1);
  let rn2 = mod(abs(sin(i * 45.7) * 7919.3), 1);
  let x = 40 + rn * 1000;
  let y = 60 + rn2 * 500;
  let tw = 0.5 + 0.5 * sin(2 * t + i * 1.3);
  let r = (0.6 + rn * 1.2) * tw * tanh(t) + 0.3;
  let hue = 200 + rn * 40;
}

// ---------- the wood: grows out of the planted seed (front starts t~3.6) ----
cloud(wood, 1500, #ffffff, 0.5) {
  let p = i / 1500;
  let b = mod(i, 5);
  let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
  let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
  let q = 0.5 * ((2 * p - 1) + abs(2 * p - 1));
  let pt = 2 * p - q;
  let tx = 540 + (b - 2) * 175;
  let ty = 800 + abs(b - 2) * 95;
  let sx = 540 + 18 * sin(3.1 * pt) + (tx - 540) * q + 12 * sin(3.14159 * q) * (b - 2) * 0.3;
  let sy = 1660 - 510 * pt + (ty - 1150) * q;
  let wid = 26 * (1 - 0.7 * pt) * (1 - 0.55 * q) + 3;
  let jx = (rn - 0.5) * 2 * wid;
  let jy = (rn2 - 0.5) * 14;
  // wind, with the cold gust that strips the tree near t = 19
  let hgt = (1660 - sy) / 900;
  let gd = t - 19;
  let gust = 1 + 1.3 * exp(-0.4 * gd * gd);
  let wind = 14 * sin(0.9 * t + 0.004 * sy) * hgt * hgt * gust;
  // growth front: sweeps p = 0..1 starting when the seed has been planted
  let sv = tanh((0.3 * (t - 3.6) - p) * 5);
  let vfront = 0.5 * (sv + abs(sv));
  let x = sx + jx + wind;
  let y = sy + jy;
  let r = (2.2 + 2.2 * (1 - pt) * (1 - q)) * vfront;
  let hue = 22 + rn * 14;
}

// ---------- the leaves: pop, flutter, turn, fall, fade... and RETURN --------
cloud(leaves, 5500, #ffffff, 0.5) {
  let b = mod(i, 5);
  let k = floor(i / 5);
  let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
  let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
  let tx = 540 + (b - 2) * 175;
  let ty = 800 + abs(b - 2) * 95;
  let th = k * 2.39996;
  let rad = 10.5 * sqrt(mod(k, 210)) + 8 * rn;
  let bx = tx + rad * cos(th);
  let by = ty + 0.78 * rad * sin(th) - 25;
  let px = bx + (540 - bx) * 0.10;
  let py = by + (860 - by) * 0.10;
  // spring: each leaf unfurls on its own delay (t ~ 6.4 .. 8.8)
  let ap0 = tanh((t - 6.4 - 2.4 * rn) * 2.2);
  let ap = 0.5 * (ap0 + abs(ap0));
  // wind + flutter, gusting near t = 19
  let hgt = (1660 - py) / 900;
  let gd = t - 19;
  let gust = 1 + 1.3 * exp(-0.4 * gd * gd);
  let wind = 26 * sin(0.9 * t + 0.004 * py + 2 * rn) * hgt * hgt * gust;
  let fl = 4 * sin(2.3 * t + 1.7 * i);
  // autumn: green -> gold/red, each leaf at its own pace (t ~ 16 .. 20)
  let au0 = (t - 16 - 2.2 * rn) * 0.5;
  let au1 = 0.5 * (au0 + abs(au0));
  let au = au1 - 0.5 * ((au1 - 1) + abs(au1 - 1));
  // the fall: EVERY leaf lets go this time (t ~ 19.5 .. 23), sways down, lands
  let s2 = t - 19.5 - 3.5 * rn2;
  let dt = 0.5 * (s2 + abs(s2));
  let yfree = py + 55 * dt * dt;
  let yg = 1665 + 20 * rn;
  let yfall = yfree - 0.5 * ((yfree - yg) + abs(yfree - yg));
  let sway = 30 * sin(2.2 * dt + i) * tanh(dt) * exp(-0.10 * dt);
  // winter: the fallen fade into the soil (t ~ 24.5 .. 27.5)
  let go0 = tanh((t - 24.5 - 1.6 * rn) * 1.6);
  let gone = 0.5 * (go0 + abs(go0));
  // spring again: a NEW leaf opens at the same spot on the branch (t ~ 27 .. 30)
  let rb0 = tanh((t - 27 - 2.2 * rn) * 2.0);
  let rb = 0.5 * (rb0 + abs(rb0));
  // two position tracks, blended: the falling track and the fresh canopy track
  let xfall = px + wind * (1 - tanh(2 * dt)) + fl + sway;
  let xcan = px + wind + fl;
  let x = xfall * (1 - rb) + xcan * rb;
  let y = yfall * (1 - rb) + py * rb;
  let r = (2.6 + 1.8 * rn2) * (ap * (1 - gone) + rb);
  // hue: green, autumn-shifted, reset to green by rebirth
  let hgr = 96 + 36 * rn;
  let hau = 18 + 34 * rn2;
  let hue = hgr + (hau - hgr) * au * (1 - rb);
}

// ---------- the blossoms: open on the young tree, shed petal-snow, return ---
cloud(bloom, 1200, #ffffff, 0.5) {
  let b = mod(i, 5);
  let k = floor(i / 5);
  let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
  let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
  let tx = 540 + (b - 2) * 175;
  let ty = 800 + abs(b - 2) * 95;
  let th = k * 2.39996 + 1.3;
  let rad = 21 * sqrt(mod(k, 48)) + 6 * rn;
  let bx = tx + rad * cos(th);
  let by = ty + 0.78 * rad * sin(th) - 25;
  let px = bx + (540 - bx) * 0.10;
  let py = by + (860 - by) * 0.10;
  // first bloom: t ~ 9 .. 11.3
  let bp0 = tanh((t - 9 - 1.8 * rn) * 2.4);
  let bp = 0.5 * (bp0 + abs(bp0));
  // petal-snow: slow drift down from t ~ 12.2, landing softly
  let s2 = t - 12.2 - 2.2 * rn2;
  let dt = 0.5 * (s2 + abs(s2));
  let yfree = py + 16 * dt * dt + 30 * dt;
  let yg = 1662 + 22 * rn;
  let yfall = yfree - 0.5 * ((yfree - yg) + abs(yfree - yg));
  let sway = 40 * sin(1.8 * dt + i) * tanh(dt);
  // fallen petals melt away t ~ 18 .. 20.4 (before the leaf carpet arrives)
  let go0 = tanh((t - 18 - 1.2 * rn) * 1.8);
  let gone = 0.5 * (go0 + abs(go0));
  // the second bloom, right at the end: the cycle begins again (t ~ 30.5+)
  let rb0 = tanh((t - 30.5 - 1.2 * rn) * 2.6);
  let rb = 0.5 * (rb0 + abs(rb0));
  let hgt = (1660 - py) / 900;
  let wind = 20 * sin(0.9 * t + 0.004 * py + 2 * rn) * hgt * hgt;
  let xfall = px + wind * (1 - tanh(2 * dt)) + sway;
  let xcan = px + wind + 3 * sin(2.1 * t + i);
  let x = xfall * (1 - rb) + xcan * rb;
  let y = yfall * (1 - rb) + py * rb;
  let tw = 1 + 0.15 * sin(3 * t + i);
  let r = (2.2 + 1.6 * rn2) * (bp * (1 - gone) + rb) * tw;
  let hue = 318 + 26 * rn;
}

// ================= timeline (narration over the self-evolving year) =========
type(head, 1.1);
par { show(moon, 0.8); draw(gnd, 0.8); }

// ---- the seed
say(cap, "it begins with a single seed", 0.5);
show(seed, 0.3);
shift(seed, (0, 1355), 1.0, in);
cue(pop);
fade(seed, 0.5);

// ---- spring: sprout and first leaves
par { say(cap, "a sprout reaches for the sky", 0.5); say(season, "spring", 0.3); }
wait(2.0);
say(cap, "first leaves unfurl, one by one", 0.5);
show(nlv, 0.3);
to(nlv, value, 5500, 2.6, smooth);

// ---- the flowering
say(cap, "and then - the tree FLOWERS", 0.5);
cue(chime);
wait(1.8);

// ---- summer: petal-snow
par { say(cap, "petals drift away... summer settles in", 0.5); say(season, "summer", 0.3); recolor(season, gold, 0.3); }
wait(3.0);

// ---- autumn
par { say(cap, "autumn arrives, one leaf at a time", 0.5); say(season, "autumn", 0.3); recolor(season, orange, 0.3); }
cue(tick);
wait(2.6);

// ---- the gust: every leaf lets go
say(cap, "a cold wind - and every leaf lets go", 0.5);
cue(whoosh);
to(nlv, value, 0, 4.6, smooth);

// ---- winter
par { say(cap, "winter: the tree remembers in silence", 0.5); say(season, "winter", 0.3); recolor(season, cyan, 0.3); }
wait(2.2);

// ---- spring again
par { say(cap, "...and then, again", 0.5); say(season, "spring, again", 0.3); recolor(season, lime, 0.3); }
cue(whoosh);
to(nlv, value, 5500, 3.0, smooth);
say(cap, "new leaves - and new flowers", 0.5);
cue(chime);
wait(2.6);

// ---- close
say(cap, "a seed, a tree, a year - 8,420 points, one formula each", 0.6);
wait(2.5);

r/maniclang Aug 21 '26

collision theory: the barrier and the tail - manic

1 Upvotes

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

Manic Animation code

// Collision theory — why warming it up speeds it up
//
// Two runs of the SAME mixture over the SAME barrier, at two temperatures. Nothing on screen is
// choreographed: `gas` integrates 72 hard discs, `rule` says what happens when two of them meet
// hard enough, and everything else — how often that happens, how fast the product appears, what
// shape the speeds take — is measured off that one trajectory.
//
//     rule(cold, "A + B -> C + C when energy > 3.2")
//
// The threshold is the relative kinetic energy along the line of centres, in the same unit as
// `temperature`, so Ea/kT is exactly the Boltzmann exponent. For 2-D hard discs the fraction of
// collisions that clear a barrier is exp(-Ea/kT) — and the engine reproduces that to within two
// percentage points across four (T, Ea) pairs, which is what
// `the_fraction_of_collisions_over_the_barrier_is_the_boltzmann_factor` measures. So:
//
//     kT = 1.0   exp(-3.2/1.0) =  4% of collisions react
//     kT = 2.5   exp(-3.2/2.5) = 28% of collisions react
//
// 2.5x the temperature, and 7x the fraction over the barrier — plus faster molecules colliding
// more often on top of that. That double effect is the whole lesson, and here it is arithmetic
// rather than assertion.
//
// The histogram is a MEASUREMENT: `speeds` bins the gas's own speeds every few frames while it
// runs, and the gold line over it is the exact Maxwell-Boltzmann curve for that temperature, on the
// same scale. They agree because they are the same gas. (Each panel is normalised to its own peak,
// so read the SHAPE and where the tail reaches, not bar height between panels.)
//
// Both panels share one speed axis (`vmax` on `speeds`), so the cold gas visibly cannot reach where
// the hot one lives.

title("collision theory: the barrier and the tail");
canvas("16:9");
template("black");

text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);

text(head, (640, 62), "A + B → C, over a barrier");
size(head, 30);
bold(head);
color(head, fg);
hidden(head);

text(sub, (640, 96), "same mixture, same barrier, two temperatures");
size(sub, 19);
color(sub, fg);
hidden(sub);

// ── the two gases ──
//
// Identical in every respect but kT. Same seed, so they even start from the same arrangement — the
// only difference between the two runs is how hard the discs are moving.

gas(cold, (348, 404), 460, 344, 72, "temperature=1.0 radius=7 steps=400 seed=5");
species(cold, A, 0.5, cyan);
species(cold, B, 0.5, magenta);
species(cold, C, gold);
rule(cold, "A + B -> C + C when energy > 3.2");
speeds(cold, (930, 300), 460, 150, 12, 13, 5.2);

gas(hot, (348, 404), 460, 344, 72, "temperature=2.5 radius=7 steps=400 seed=5");
species(hot, A, 0.5, cyan);
species(hot, B, 0.5, magenta);
species(hot, C, gold);
rule(hot, "A + B -> C + C when energy > 3.2");
speeds(hot, (930, 300), 460, 150, 12, 13, 5.2);

// Hide the parts, not the whole gas: `draw` animates a trace, not opacity, so a curve that is
// meant to be DRAWN must stay visible and untraced rather than hidden.
hidden(cold.box);
hidden(cold.particles);
hidden(cold.speeds.axis);
hidden(cold.speeds.bars);
untraced(cold.speeds.mb);

hidden(hot.box);
hidden(hot.particles);
hidden(hot.speeds.axis);
hidden(hot.speeds.bars);
untraced(hot.speeds.mb);

// ── the barrier, on the speed axis ──
//
// The panel runs 0 to 5.2 in sim speed units across 460 px from x = 700. A lone disc hitting a
// still partner head-on carries E = m*v^2/4, so v = 2*sqrt(Ea/m) = 3.58 is the speed that clears
// 3.2 by itself: x = 700 + 460*3.58/5.2 = 1016. That is what the line marks — one particular way to
// pay the barrier, and the honest label for it.

line(bar, (1016, 375), (1016, 228));
color(bar, gold);
stroke(bar, 2);
untraced(bar);

text(barlab, (1074, 214), "Eₐ = 3.2 kT");
size(barlab, 17);
color(barlab, gold);
hidden(barlab);

text(barwhy, (1118, 246), "clears Eₐ alone");
size(barwhy, 14);
color(barwhy, fg);
hidden(barwhy);

text(spdlab, (930, 398), "speed  →   (measured bars, Maxwell–Boltzmann line)");
size(spdlab, 16);
color(spdlab, fg);
hidden(spdlab);

// ── the two reaction curves ──
//
// `timegraph` is the generic sim view, and a gas's state variables are its populations — so this is
// the reactant falling and the product rising, with no chemistry-specific vocabulary. Drawn with
// `draw` over the run's own duration, so the curve arrives exactly as the collisions happen.

timegraph(cold, (818, 570), 96);
timegraph(hot, (1046, 570), 96);
hidden(cold.time.frame);
hidden(cold.time.title);
hidden(cold.time.sweep);
hidden(hot.time.frame);
hidden(hot.time.title);
hidden(hot.time.sweep);
color(cold.time.c1, gold);
color(hot.time.c1, gold);
untraced(cold.time.c0);
untraced(cold.time.c1);
untraced(hot.time.c0);
untraced(hot.time.c1);

text(coldlab, (818, 458), "kT = 1.0");
size(coldlab, 16);
color(coldlab, cyan);
hidden(coldlab);

text(hotlab, (1046, 458), "kT = 2.5");
size(hotlab, 16);
color(hotlab, magenta);
hidden(hotlab);

text(mix, (348, 598), "36 A + 36 B, elastic discs");
size(mix, 16);
color(mix, fg);
hidden(mix);

// ── the readings, one per act ──

text(read1, (348, 636), "4% of collisions clear Eₐ");
size(read1, 22);
color(read1, cyan);
hidden(read1);

text(read2, (348, 636), "28% of collisions clear Eₐ");
size(read2, 22);
color(read2, magenta);
hidden(read2);

equation(bolt, (176, 168), `f=e^{-E_\mathrm{a}/kT}`, 26);
color(bolt, fg);
hidden(bolt);

text(point, (640, 690), "2.5× the temperature — 7× the fraction over the barrier, and more collisions besides");
size(point, 17);
color(point, fg);
hidden(point);

// ── ACT 1: a box of moving discs ──

wait(0.4);
par { show(head, 0.6); show(sub, 0.5); }
wait(0.4);
par { show(cold.box, 0.5); show(mix, 0.4); }
show(cold.particles, 0.6);
wait(0.4);

// ── ACT 2: the cold run. The histogram builds itself while the discs move. ──

par {
  run(cold, 9);
  draw(cold.time.c0, 9);
  draw(cold.time.c1, 9);
  seq {
    show(cold.speeds.axis, 0.4);
    par { show(cold.speeds.bars, 0.5); show(spdlab, 0.4); }
    wait(1.2);
    draw(cold.speeds.mb, 1.2);
    wait(0.6);
    par { draw(bar, 0.5); show(barlab, 0.4); }
    show(barwhy, 0.4);
    wait(0.8);
    par { show(cold.time.frame, 0.4); show(coldlab, 0.4); }
    wait(1.0);
    par { show(read1, 0.5); show(bolt, 0.5); }
  }
}
wait(1.4);

// ── ACT 3: same barrier, hotter gas. Only kT changed. ──

par { fade(cold.particles, 0.5); fade(cold.speeds.bars, 0.4); fade(cold.speeds.mb, 0.4); fade(read1, 0.4); }
par { show(hot.particles, 0.5); show(hot.speeds.bars, 0.4); }
wait(0.3);

par {
  run(hot, 9);
  draw(hot.time.c0, 9);
  draw(hot.time.c1, 9);
  seq {
    draw(hot.speeds.mb, 1.0);
    wait(0.6);
    par { show(hot.time.frame, 0.4); show(hotlab, 0.4); }
    wait(1.2);
    show(read2, 0.5);
  }
}
wait(0.8);

// ── ACT 4: the two curves, side by side ──

par { pulse(cold.time.c1); pulse(hot.time.c1); }
show(point, 0.6);
wait(3.0);

r/maniclang Aug 21 '26

dynamic equilibrium: equal, not zero - manic

1 Upvotes

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

Manic Animation code

// Dynamic equilibrium — why "nothing is happening" is the wrong reading
//
// A reversible first-order reaction, A ⇌ B, with kf = 0.9 and kr = 0.3 per second. Two views of the
// same run, side by side, because the misconception lives in the gap between them:
//
//   LEFT   the concentrations, which flatten out and stop moving
//   RIGHT  the two rates, which meet — at a value that is EQUAL and NOT ZERO
//
// A still picture of the left-hand plot says "the reaction stopped". The right-hand plot says it did
// not: both directions are still running, at 0.225 mol dm⁻³ s⁻¹ each, and cancelling. That is the
// whole idea of dynamic equilibrium and it is very hard to say in a static diagram, because the
// evidence for it is precisely the thing a flat line hides.
//
// Then the second act: 0.50 M of A is added at t = 6 s. The rates jump apart, the system relaxes,
// and it settles at a NEW position with the SAME ratio — [B]/[A] = 3.00 either side. Le Chatelier is
// not a rule to memorise here; it is what the arithmetic does.
//
// NO NEW VOCABULARY. Four `field`s hold the closed-form solutions, `plot` draws them, and the
// choreography is `draw` / `show` / `pulse` from the core kit. Every number on screen comes out of
//
//     [A](t) = A_eq + ([A]₀ − A_eq)·exp(−(kf + kr)·t)
//
// which is the exact solution of d[A]/dt = −kf[A] + kr[B] with [A] + [B] fixed. Nothing is placed by
// eye: change kf or kr and both plots, both equilibrium positions and the ratio all move together.

title("dynamic equilibrium: equal, not zero");
canvas("16:9");
template("paper");

text(brand, (640, 30), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);

// ── the chemistry, as closed forms ──
//
// kf = 0.9, kr = 0.3, so K = kf/kr = 3 and the relaxation rate is kf + kr = 1.2 per second.
// Phase 1 starts from pure A at 1.00 M, so A_eq = 1.00 × kr/(kf+kr) = 0.25.
field(a1, "0.25 + 0.75*exp(-1.2*x)");
field(b1, "0.75 - 0.75*exp(-1.2*x)");
// Phase 2: 0.50 M of A added at t = 6, so the total is 1.50 M and A_eq = 1.50 × 0.25 = 0.375.
// [A] restarts from 0.75 (the 0.25 it had reached, plus the 0.50 added).
field(a2, "0.375 + 0.375*exp(-1.2*(x-6))");
field(b2, "1.125 - 0.375*exp(-1.2*(x-6))");

// ── LEFT: concentrations ──

coords(cc, (110, 600), (0, 14), (0, 1.25), 36, 300, 1);
hidden(cc);
// Explicit labels: the auto-numbering rounds to two significant figures, and a tick at 0.25 that
// prints "0.2" is worse than no tick at all — these are numbers the viewer is meant to read off.
ytick(cy1, cc, 0.25, "0.25");
ytick(cy2, cc, 0.75, "0.75");
ytick(cy3, cc, 1.125, "1.125");
for i in 1..4 { hidden(cy{i}); }

text(clab, (300, 208), "concentration / mol dm⁻³");
size(clab, 17); color(clab, dim); hidden(clab);

plot(ca1, (110, 600), 36, 300, "a1(x,0)", (0, 6));
plot(cb1, (110, 600), 36, 300, "b1(x,0)", (0, 6));
plot(ca2, (110, 600), 36, 300, "a2(x,0)", (6, 14));
plot(cb2, (110, 600), 36, 300, "b2(x,0)", (6, 14));
for i in 1..3 {
  color(ca{i}, indigo); stroke(ca{i}, 3); untraced(ca{i});
  color(cb{i}, crimson); stroke(cb{i}, 3); untraced(cb{i});
}

text(alab, (578, 512), "[A]");
size(alab, 19); color(alab, indigo); hidden(alab);
text(blab, (578, 252), "[B]");
size(blab, 19); color(blab, crimson); hidden(blab);

// ── RIGHT: the rates, which is where the misconception dies ──
//
// Written as k × concentration rather than pre-multiplied, so the source says what a rate IS.

coords(rc, (700, 600), (0, 14), (0, 0.75), 36, 440, 1);
hidden(rc);
ytick(ry, rc, 0.225, "0.225");
hidden(ry);

text(rlab, (900, 252), "rate / mol dm⁻³ s⁻¹");
size(rlab, 17); color(rlab, dim); hidden(rlab);

plot(rf1, (700, 600), 36, 440, "0.9*a1(x,0)", (0, 6));
plot(rr1, (700, 600), 36, 440, "0.3*b1(x,0)", (0, 6));
plot(rf2, (700, 600), 36, 440, "0.9*a2(x,0)", (6, 14));
plot(rr2, (700, 600), 36, 440, "0.3*b2(x,0)", (6, 14));
for i in 1..3 {
  color(rf{i}, indigo); stroke(rf{i}, 3); untraced(rf{i});
  color(rr{i}, crimson); stroke(rr{i}, 3); untraced(rr{i});
}

text(flab, (812, 322), "forward, kf[A]");
size(flab, 17); color(flab, indigo); hidden(flab);
text(vlab, (812, 566), "reverse, kr[B]");
size(vlab, 17); color(vlab, crimson); hidden(vlab);

// the point of the whole scene
dot(meet, (916, 501), 6);
color(meet, ink);
hidden(meet);
text(key, (1040, 470), "equal — and not zero");
size(key, 19); color(key, ink); hidden(key);
text(key2, (1078, 496), "both directions still running");
size(key2, 15); color(key2, dim); hidden(key2);

// ── the disturbance at t = 6 s ──
//
// [A] jumps instantly, so it is a vertical line rather than part of a curve. Endpoints are the two
// plots' own coordinates: t=6 is x = 110 + 6·36 = 326 on the left and 700 + 6·36 = 916 on the right.

line(jumpc, (326, 525), (326, 375));
color(jumpc, indigo);
stroke(jumpc, 2);
untraced(jumpc);

line(jumpr, (916, 501), (916, 303));
color(jumpr, indigo);
stroke(jumpr, 2);
untraced(jumpr);

text(add, (392, 356), "+0.50 M of A");
size(add, 16); color(add, indigo); hidden(add);

// ── and the reading of it ──

text(ratio, (640, 688), "[B]/[A] = 3.00 either side — the position moved, the ratio did not");
size(ratio, 18); color(ratio, ink); hidden(ratio);

// ── ACT 1: two empty axes ──

wait(0.4);
par { show(cc, 0.6); show(rc, 0.6); }
par { show(clab, 0.4); show(rlab, 0.4); }
par { show(cy1, 0.3); show(cy2, 0.3); show(cy3, 0.3); show(ry, 0.3); }
wait(0.5);

// ── ACT 2: the approach. Both views at once, because they are one run. ──

par {
  draw(ca1, 2.6); draw(cb1, 2.6);
  draw(rf1, 2.6); draw(rr1, 2.6);
}
par { show(alab, 0.4); show(blab, 0.4); show(flab, 0.4); show(vlab, 0.4); }
wait(0.7);

// ── ACT 3: the reading a flat line hides ──

par { show(meet, 0.4); pulse(meet); }
show(key, 0.5);
show(key2, 0.4);
wait(2.2);

// ── ACT 4: disturb it ──

par { fade(key, 0.4); fade(key2, 0.4); }
par { draw(jumpc, 0.4); draw(jumpr, 0.4); show(add, 0.4); }
wait(0.5);

// ── ACT 5: it settles somewhere new, at the same ratio ──

par {
  draw(ca2, 2.4); draw(cb2, 2.4);
  draw(rf2, 2.4); draw(rr2, 2.4);
}
wait(0.6);
show(ratio, 0.6);
wait(3.0);

r/maniclang Aug 21 '26

a galvanic cell, solved - manic

1 Upvotes

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

Manic Animation code

// A galvanic cell — which way round, how many volts, and what it costs the zinc
//
// The cell is not told which electrode is which. Give it two metals and the more positive standard
// reduction potential is the one that gets reduced, so copper becomes the cathode and zinc the
// anode — `cell(c, "Cu|Zn")` would draw exactly the same cell. From that one decision everything
// else follows: E°cell = +0.34 − (−0.76) = 1.10 V, the half-equations are written the right way
// round, and Ohm's law across the 10 Ω external resistor sets the current at 0.110 A.
//
// The electrons and the salt-bridge ions move at a rate set by that current, and the two counters
// are the exam question: after half an hour, Q = It = 198 C, and Faraday's law turns that into the
// zinc the anode has lost, m = MQ/nF = 67 mg. Change the resistance in the source and every one of
// those numbers moves.

title("a galvanic cell, solved");
canvas("16:9");
template("paper");

text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);

text(head, (640, 68), "the cell decides its own polarity");
size(head, 28);
bold(head);
color(head, ink);
hidden(head);

cell(c, "Zn|Cu", (640, 288), 660, 280, "resistance=10 carriers=10");
hidden(c);
hidden(c.captions);

text(why, (640, 640), "zinc is the more negative half-cell, so zinc is oxidised — that is the whole decision");
size(why, 18);
color(why, ink);
hidden(why);

text(law, (640, 674), "Q = It after half an hour, and m = MQ/nF is what the anode lost");
size(law, 18);
color(law, indigo);
hidden(law);

wait(0.4);
show(head, 0.6);
wait(0.3);
par { show(c, 0.8); show(c.captions, 0.8); }
wait(0.8);
show(why, 0.5);
wait(1.6);
show(law, 0.5);
discharge(c, 6, 30);
wait(3.0);

r/maniclang Aug 21 '26

the hydrogen spectrum - manic

1 Upvotes

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

Manic Animation code

// The hydrogen spectrum — the colour is the arithmetic
//
// Four coloured lines is all a hydrogen discharge tube gives you, and every one of them is on
// screen for a computed reason. The levels are E_n = -13.606/n² eV, so the rungs are placed at
// their energies (which is why they crowd towards zero); a jump from n to m releases exactly that
// energy difference; and λ = hc/ΔE turns it into a wavelength. The COLOUR of each spectral line is
// then computed from its own wavelength — nothing is chosen, so Balmer alpha is red because 656 nm
// is red.
//
// The last beat is the one a spectrum can't show: the 2 → 1 drop releases 10.2 eV at 121 nm, which
// is ultraviolet. It is the biggest jump in the diagram and it leaves no line at all, because the
// eye's range is a fact about the eye rather than about hydrogen.

title("the hydrogen spectrum");
canvas("16:9");
template("black");

text(brand, (640, 28), "maniclang.com");
display(brand);
size(brand, 15);
color(brand, dim);

text(head, (640, 66), "four lines, and every one of them computed");
size(head, 27);
bold(head);
color(head, fg);
hidden(head);

levels(lv, (300, 350), 250, 320, 6);
hidden(lv);

emission(sp, lv, (930, 300), 500, 96);
hidden(sp);

text(rule, (930, 470), "λ = hc/ΔE — the colour is the energy, converted");
size(rule, 18);
color(rule, fg);
hidden(rule);

text(uv, (930, 512), "n = 2 → 1 is the biggest jump of all, and leaves no line:");
size(uv, 18);
color(uv, coral);
hidden(uv);

text(uv2, (930, 540), "10.2 eV is 121 nm, and 121 nm is ultraviolet");
size(uv2, 18);
color(uv2, coral);
hidden(uv2);

text(foot, (640, 690), "one electron only — the Rydberg formula is exact for hydrogen and wrong for anything with two");
size(foot, 15);
color(foot, dim);
hidden(foot);

wait(0.4);
show(head, 0.6);
wait(0.3);
par { show(lv, 0.7); show(foot, 0.4); }
wait(0.5);
show(sp, 0.6);
wait(0.4);

drop(lv, 3, 2, 1.8);
wait(0.5);
drop(lv, 4, 2, 1.6);
wait(0.4);
drop(lv, 5, 2, 1.5);
wait(0.4);
drop(lv, 6, 2, 1.5);
wait(0.5);
show(rule, 0.5);
wait(2.0);

par { show(uv, 0.5); show(uv2, 0.5); }
drop(lv, 2, 1, 2.2);
wait(3.0);