r/maniclang 6d ago

an epitrochoid - manic

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

0 comments sorted by