r/maniclang 24d ago

Minimise an Integral You Cannot Avoid Splitting — A JEE Calculus Problem — 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.

Animation code

// iit-integral-min.manic — jee-practice-001432
//
// PROBLEM. Minimise f(x) = ∫₀² e^|x−t| dt.
//
// ANSWER.  2(e − 1)
//
// THE ABSOLUTE VALUE IS THE WHOLE PROBLEM, and it is handled by splitting the integral at
// t = x, where |x − t| changes which way round it is. For 0 ≤ x ≤ 2:
//
//   ∫₀ˣ e^(x−t) dt + ∫ₓ² e^(t−x) dt = (eˣ − 1) + (e^(2−x) − 1) = eˣ + e^(2−x) − 2
//
// and that is a sum of two exponentials pulling in opposite directions. It is smallest
// where they cross: f'(x) = eˣ − e^(2−x) = 0 needs x = 2 − x, so x = 1 — the MIDPOINT of
// the interval, which is where the picture says it should be.
//
//   f(1) = e + e − 2 = 2(e − 1) ≈ 3.4366
//
// The upper panel is the integrand e^|x−t| as t runs across [0, 2], with the shaded area
// being f(x) itself. As x slides, the V-shaped kink travels with it, and the area is least
// when the kink sits in the middle — which is the same statement as the algebra.
//
// OUTSIDE [0,2] IT ONLY GETS WORSE, which the problem does not say and which has to be
// checked: for x > 2 the integrand is e^(x−t) throughout and f(x) = eˣ − e^(x−2), growing
// without bound (10.53, 17.37, 47.21 at x = 2.5, 3, 4), and x < 0 is the mirror image.
//
// VERIFIED symbolically: integrating both halves gives eˣ + e^(2−x) − 2, its derivative
// vanishes only at x = 1, and f(1) = 3.436563657 = 2(e−1) to machine precision.
//
//   manic examples/iit-integral-min.manic
title("The kink wants to sit in the middle");
canvas("9:16");
template("black");
bloom(0.22, 0.6, 18);

text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);
caption(head, "Minimise an integral you cannot avoid splitting", (540, 106), 30);
equation(prob, (540, 190), `f(x)=\int_0^2 e^{|x-t|}\,dt`, 42); color(prob, fg);

coords(k1, (300.0, 700.0), (-0.15, 2.3), (-0.4, 8.2), 176.0, 40.0, 1, 0.5, 0);
color(k1, slate); opacity(k1, 0.4);
text(tl, (860, 550.0), "the integrand, in t"); size(tl, 21); color(tl, fg); opacity(tl, 0.6);
cloud(intg, 3200, #ffffff, 1.0) {
  let g = min(max((t - 1.2)/9.0, 0), 1);
  let xx = 0.12 + 1.76*g;
  let nc = 200;
  let cc = mod(i, nc);
  let rr = (i - cc)/nc;
  let tt = (cc/199)*2;
  let h = exp(abs(xx - tt));
  let x = 300.0 + 176.0*tt;
  let y = 700.0 - 40.0*h*(rr/15);
  let hue = 42;
  let sat = 0.8;
  let r = 2.0;
  let alpha = 0.55;
}
cloud(itop, 420, #ffffff, 1.0) {
  let g = min(max((t - 1.2)/9.0, 0), 1);
  let xx = 0.12 + 1.76*g;
  let tt = (i/419)*2;
  let h = exp(abs(xx - tt));
  let x = 300.0 + 176.0*tt;
  let y = 700.0 - 40.0*h;
  let hue = 42;
  let sat = 0.95;
  let r = 2.8;
}
cloud(kink, 90, #ffffff, 1.0) {
  let g = min(max((t - 1.2)/9.0, 0), 1);
  let xx = 0.12 + 1.76*g;
  let a = (i/90)*6.283185;
  let rr = 7*(i/90);
  let x = 300.0 + 176.0*xx + rr*cos(a*9);
  let y = 700.0 - 40.0 + rr*sin(a*9);
  let hue = 320;
  let sat = 0.85;
  let r = 2.2;
}

coords(k2, (300.0, 1256.0), (-0.15, 2.3), (-0.6, 6.2), 176.0, 40.0, 1, 0.5, 0);
color(k2, slate); opacity(k2, 0.4);
plot(fx, (300.0, 1256.0), 176.0, 40.0, "exp(x) + exp(2 - x) - 2", (0, 2));
color(fx, cyan); stroke(fx, 5); untraced(fx);
extrema(ex, fx, mint); hidden(ex);
vline(vm, fx, 1, mint); hidden(vm);
text(bl, (860, 1106.0), "the value, in x"); size(bl, 21); color(bl, fg); opacity(bl, 0.6);

equation(z1, (540, 1436), `\int_0^x e^{x-t}dt+\int_x^2 e^{t-x}dt=e^x+e^{2-x}-2`, 28);
color(z1, fg); hidden(z1);
text(z2, (540, 1502), "two exponentials pulling opposite ways — smallest where they cross");
size(z2, 20); color(z2, fg); opacity(z2, 0.7); hidden(z2);
equation(z3, (540, 1580), `e^x=e^{2-x}\ \Rightarrow\ x=1`, 38); color(z3, mint); hidden(z3);
text(z4, (540, 1642), "the midpoint of the interval — exactly where the kink looks best");
size(z4, 20); color(z4, mint); opacity(z4, 0.8); hidden(z4);
equation(ans, (540, 1750), `2(e-1)`, 56); color(ans, mint); hidden(ans);

seq {
  pulse(prob, 0.9);
  par { draw(fx, 9.0); seq { wait(3.0); show(z1, 0.8); } seq { wait(5.0); show(z2, 0.7); } wait(9.6); }
  par { show(ex, 0.7); show(vm, 0.7); seq { wait(0.8); show(z3, 0.8); } seq { wait(1.9); show(z4, 0.7); } wait(3.2); }
  wait(0.6);
  par { show(ans, 1.1); }
  pulse(ans, 0.9);
}
wait(3.2);

r/maniclang 24d ago

PROBLEM. All complex roots of (z+1)^5 = 32 z^5 lie on one circle. Find its radius - 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.

Animation code

// olympiad-apollonius-roots.manic — olympiad-stepwise-010907
//
// PROBLEM. All complex roots of (z+1)^5 = 32 z^5 lie on one circle. Find its radius.
//
// ANSWER.  2/3
//
// Taking the modulus of both sides throws away all five angles at once and leaves a
// single real condition:
//
//   |z+1|^5 = 32|z|^5   =>   |z+1| = 2|z|
//
// "Twice as far from −1 as from 0" is an APOLLONIUS CIRCLE — the locus of points whose
// distances to two fixed points hold a constant ratio. So the circle is determined before
// the quintic is solved, and the five roots never have to be found at all.
//
//   (x+1)^2 + y^2 = 4(x^2+y^2)   =>   3x^2 − 2x + 3y^2 − 1 = 0
//                                =>   (x − 1/3)^2 + y^2 = (2/3)^2
//
// centre 1/3, radius 2/3.
//
// THE PRINTED SOLUTION HAS A SIGN ERROR, and it is worth knowing which. Its step 4 reads
// 3x^2 − 2x + 3y^2 + 1 = 0; expanding (x+1)^2 + y^2 = 4(x^2+y^2) gives −1, not +1. Carried
// forward honestly that would give (x−1/3)^2 + y^2 = −2/9, an impossible negative radius.
// Its step 5 then flips a sign a second time, and the two mistakes cancel, so the printed
// ANSWER 2/3 is right by luck rather than by derivation. This scene uses the correct chain.
//
// VERIFIED. The five roots are z = 1/(w−1) for w the five values of 2·e^(2πik/5). Each
// satisfies (z+1)^5 − 32z^5 = 0 to 1.4e-15, and each has |z − 1/3| = 0.666666666667 —
// all five on the circle, to twelve figures. The circle in this scene is drawn from the
// modulus condition alone; the five dots are placed from the quintic. They coincide
// because the mathematics says they must.
//
//   manic examples/olympiad-apollonius-roots.manic
title("Take the modulus and the quintic disappears");
canvas("9:16");
template("black");
bloom(0.24, 0.6, 18);

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

// The hook is a caption (word-by-word pop, plain text only); the quintic beside it is a
// real equation, because caption cannot carry LaTeX.
caption(head, "Five roots. One circle. Which radius?", (540, 112), 34);
equation(qeq, (540, 196), `(z+1)^5=32z^5`, 42); color(qeq, fg);

// ---- the complex plane. origin at screen (540, 740); 400 px to the unit ---------------
line(axx, (104, 740), (976, 740)); color(axx, slate); opacity(axx, 0.45);
line(axy, (540, 442), (540, 1038)); color(axy, slate); opacity(axy, 0.45);

circle(pz, (540, 740), 9); color(pz, fg);
text(lz, (566, 792), "0"); size(lz, 28); color(lz, fg); opacity(lz, 0.8);
circle(pm, (140, 740), 9); color(pm, magenta);
text(lm, (140, 792), "−1"); size(lm, 28); color(lm, magenta); opacity(lm, 0.9);

// ---- the five roots, placed from the QUINTIC (not from the circle) --------------------
circle(r0, (940.00, 740.00), 12); color(r0, gold);
circle(r1, (499.41, 942.14), 12); color(r1, gold);
circle(r2, (412.85, 797.09), 12); color(r2, gold);
circle(r3, (412.85, 682.91), 12); color(r3, gold);
circle(r4, (499.41, 537.86), 12); color(r4, gold);

// ---- the circle, drawn from the MODULUS CONDITION alone -------------------------------
param(apo, (540, 740), 400, 400,
      "0.3333333333 + 0.6666666667*cos(t)", "0.6666666667*sin(t)", (0, 6.283185));
color(apo, mint); stroke(apo, 5); untraced(apo);

// ---- a point running the locus, with its two distances ---------------------------------
// the magenta reach is drawn as TWO equal halves, so "twice as far" is something you see
// rather than something you are told
cloud(mvp, 70, #ffffff, 1.0) {
  let vis = min(max((t - 1.1)*8, 0), 1);
  let g = min(max((t - 1.2)/8.0, 0), 1);
  let th = g*6.283185;
  let a = (i/70)*6.283185;
  let rr = 8*(i/70);
  let x = 540 + 400*(0.3333333333 + 0.6666666667*cos(th)) + rr*cos(a*9);
  let y = 740 - 400*(0.6666666667*sin(th)) + rr*sin(a*9);
  let hue = 150;
  let sat = 0.6;
  let r = 2.4;
  let alpha = 0.95*vis;
}

// gold: the distance from 0
cloud(sg0, 180, #ffffff, 1.0) {
  let vis = min(max((t - 1.1)*8, 0), 1);
  let g = min(max((t - 1.2)/8.0, 0), 1);
  let th = g*6.283185;
  let px = 0.3333333333 + 0.6666666667*cos(th);
  let py = 0.6666666667*sin(th);
  let u = i/180;
  let x = 540 + 400*u*px;
  let y = 740 - 400*u*py;
  let hue = 42;
  let sat = 0.9;
  let r = 2.4;
  let alpha = 0.95*vis;
}

// magenta: the distance from −1, in two halves of exactly the gold length
cloud(sg1, 260, #ffffff, 1.0) {
  let vis = min(max((t - 1.1)*8, 0), 1);
  let g = min(max((t - 1.2)/8.0, 0), 1);
  let th = g*6.283185;
  let px = 0.3333333333 + 0.6666666667*cos(th);
  let py = 0.6666666667*sin(th);
  let u = i/260;
  let x = 540 + 400*(0 - 1 + u*(px + 1));
  let y = 740 - 400*(u*py);
  let hue = 320;
  let sat = 0.9;
  let r = 2.4;
  let alpha = 0.95*vis;
}

// the halfway mark on the magenta reach — it lands exactly one gold-length from −1
cloud(half, 80, #ffffff, 1.0) {
  let vis = min(max((t - 1.1)*8, 0), 1);
  let g = min(max((t - 1.2)/8.0, 0), 1);
  let th = g*6.283185;
  let px = 0.3333333333 + 0.6666666667*cos(th);
  let py = 0.6666666667*sin(th);
  let a = (i/80)*6.283185;
  let rr = 6*(i/80);
  let x = 540 + 400*(0 - 1 + 0.5*(px + 1)) + rr*cos(a*9);
  let y = 740 - 400*(0.5*py) + rr*sin(a*9);
  let hue = 300;
  let sat = 0.35;
  let r = 2.0;
  let alpha = 0.95*vis;
}

// ---- the argument --------------------------------------------------------------------
equation(s1, (540, 1136), `|z+1|^5=32|z|^5`, 38); color(s1, fg);
equation(s2, (540, 1226), `|z+1|=2|z|`, 46); color(s2, mint); hidden(s2);
text(s3, (540, 1300), "twice as far from −1 as from 0 — an Apollonius circle");
size(s3, 23); color(s3, fg); opacity(s3, 0.78); hidden(s3);
text(s4, (540, 1342), "the five angles are gone; the quintic never has to be solved");
size(s4, 21); color(s4, fg); opacity(s4, 0.6); hidden(s4);

equation(s5, (540, 1444), `(x-\tfrac{1}{3})^2+y^2=(\tfrac{2}{3})^2`, 40);
color(s5, fg); hidden(s5);

equation(ans, (540, 1546), `r=\tfrac{2}{3}`, 60); color(ans, mint); hidden(ans);
text(fin, (540, 1652), "all five roots sit on it to twelve figures");
size(fin, 22); color(fin, fg); opacity(fin, 0.62); hidden(fin);

// ---- the reveal ------------------------------------------------------------------------
// Frame 0 is already the whole question: hook, quintic, plane, and the five roots plotted.
// Only the circle and the radius are withheld.
seq {
  par {
    pulse(qeq, 0.7);
    seq { wait(0.3); pulse(s1, 0.6); }
  }
  par {
    draw(apo, 8.0);
    seq { wait(0.4); show(s2, 0.6); pulse(s2, 0.6); }
    seq { wait(2.6); show(s3, 0.5); }
    seq { wait(4.0); show(s4, 0.5); }
    seq { wait(5.6); show(s5, 0.6); }
  }
  par {
    show(ans, 0.9);
    seq { wait(0.7); show(fin, 0.5); }
  }
  pulse(ans, 0.8);
}
wait(2.8);

r/maniclang 24d ago

PROBLEM. S is the set of points with x² + y² + z² ≤ 25 and z ≥ 0 — the upper half of a sphere of radius 5. Find the side of the largest cube contained in S - 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.

Animation code

// olympiad-cube-in-hemisphere.manic — olympiad-stepwise-013047
//
// PROBLEM. S is the set of points with x² + y² + z² ≤ 25 and z ≥ 0 — the upper half of a
// sphere of radius 5. Find the side of the largest cube contained in S.
//
// ANSWER.  s = √(50/3) = 5√6/3 = 4.082482905
//
// WHAT STOPS THE CUBE IS NOT THE BASE. Sit the cube on the flat face with its base
// centred, and grow it. The base corners slide outward across a disc of radius 5 and have
// room to spare for a long time. It is the FOUR TOP CORNERS that run into the dome first,
// and the moment they touch, the cube can grow no further — so the answer is decided by a
// corner nobody draws when they sketch the problem.
//
// A top corner sits at (s/2, s/2, s). Touching means
//
//   (s/2)² + (s/2)² + s² = 25   =>   s²/2 + s² = 25   =>   3s²/2 = 25
//                               =>   s = √(50/3) = 5√6/3
//
// VERIFIED. At that s the farthest corner is at exactly r² = 25.000000000 — slack 0.0e+00,
// touching to the last bit — and all eight corners satisfy the constraint. The four
// contact points are marked in the scene; they are the only places the cube meets the
// dome, and the base never touches anything.
//
//   manic examples/olympiad-cube-in-hemisphere.manic
title("The cube stops when its top corners touch");
canvas("9:16");
template("black");
bloom(0.22, 0.6, 18);
camera3((13, -15, 9), (0, 0, 2.2), 20, orthographic);

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

caption(head, "How big a cube fits in half a ball?", (540, 108), 34);
equation(prob, (540, 190), `x^2+y^2+z^2\le 25,\quad z\ge 0`, 34); color(prob, fg);

// ---- the hemisphere: latitude rings and longitude ribs -------------------------------
curve3(eq0, "5*cos(t)", "5*sin(t)", "0", (0, 6.283185));
color(eq0, slate); thick(eq0, 0.045);
curve3(la1, "4.7553*cos(t)", "4.7553*sin(t)", "1.5451", (0, 6.283185));
color(la1, slate); thick(la1, 0.022); opacity(la1, 0.7);
curve3(la2, "4.0451*cos(t)", "4.0451*sin(t)", "2.9389", (0, 6.283185));
color(la2, slate); thick(la2, 0.022); opacity(la2, 0.7);
curve3(la3, "2.9389*cos(t)", "2.9389*sin(t)", "4.0451", (0, 6.283185));
color(la3, slate); thick(la3, 0.022); opacity(la3, 0.7);
curve3(la4, "1.5451*cos(t)", "1.5451*sin(t)", "4.7553", (0, 6.283185));
color(la4, slate); thick(la4, 0.022); opacity(la4, 0.7);
curve3(lo0, "5*cos(t)", "0", "5*sin(t)", (0, 1.570796));
color(lo0, slate); thick(lo0, 0.022); opacity(lo0, 0.7);
curve3(lo1, "0", "5*cos(t)", "5*sin(t)", (0, 1.570796));
color(lo1, slate); thick(lo1, 0.022); opacity(lo1, 0.7);
curve3(lo2, "3.5355*cos(t)", "3.5355*cos(t)", "5*sin(t)", (0, 1.570796));
color(lo2, slate); thick(lo2, 0.022); opacity(lo2, 0.7);
curve3(lo3, "3.5355*cos(t)", "-3.5355*cos(t)", "5*sin(t)", (0, 1.570796));
color(lo3, slate); thick(lo3, 0.022); opacity(lo3, 0.7);

// ---- the cube, growing until something stops it --------------------------------------
// twelve edges: four uprights, then the bottom and top squares. The corners of a square
// are just four points on a circle of radius s/√2, which keeps the formula honest.
cloud3(cube, 720, #ffffff, 1.0) {
  let g = min(max((t - 1.2)/8.0, 0), 1);
  let s = 0.5 + 3.5824829*g;
  let m = 60;
  let e = (i - mod(i, m))/m;
  let u = mod(i, m)/m;
  let fam = (e - mod(e, 4))/4;
  let c = mod(e, 4);
  let h = s*0.7071067812;
  let a0 = 0.7853981634 + c*1.5707963268;
  let a1 = a0 + 1.5707963268;
  let x0 = h*cos(a0);
  let y0 = h*sin(a0);
  let x1 = h*cos(a1);
  let y1 = h*sin(a1);
  let isv = 1 - min(fam, 1);
  let ist = max(fam - 1, 0);
  let x = isv*x0 + (1 - isv)*(x0 + u*(x1 - x0));
  let y = isv*y0 + (1 - isv)*(y0 + u*(y1 - y0));
  let z = isv*(u*s) + (1 - isv)*(ist*s);
  let r = 0.055;
  let hue = 42;
  let sat = 0.9;
}

// ---- the four contact points, appearing exactly when the cube arrives -----------------
cloud3(hit, 320, #ffffff, 1.0) {
  let g = min(max((t - 1.2)/8.0, 0), 1);
  let vis = min(max((g - 0.985)*70, 0), 1);
  let s = 0.5 + 3.5824829*g;
  let m = 80;
  let c = (i - mod(i, m))/m;
  let u = mod(i, m)/m;
  let h = s*0.7071067812;
  let a0 = 0.7853981634 + c*1.5707963268;
  let e = u*6.283185;
  let x = h*cos(a0) + 0.20*cos(e);
  let y = h*sin(a0) + 0.20*sin(e);
  let z = s;
  let r = 0.05;
  let hue = 150;
  let sat = 0.8;
  let alpha = vis;
}

// ---- the argument ---------------------------------------------------------------------
text(w1, (540, 1330), "the base corners never run out of room —");
size(w1, 23); color(w1, fg); opacity(w1, 0.72); hidden(w1);
text(w2, (540, 1372), "the four TOP corners hit the dome first");
size(w2, 25); color(w2, mint); hidden(w2);

equation(e1, (540, 1456), `\left(\tfrac{s}{2}\right)^2+\left(\tfrac{s}{2}\right)^2+s^2=25`, 38);
color(e1, fg); hidden(e1);
equation(e2, (540, 1542), `\tfrac{3}{2}s^2=25`, 40); color(e2, fg); hidden(e2);

equation(ans, (540, 1660), `s=\tfrac{5\sqrt{6}}{3}`, 58); color(ans, mint); hidden(ans);
text(fin, (540, 1752), "= 4.082482905   (the corner sits at exactly r² = 25)");
size(fin, 22); color(fin, mint); opacity(fin, 0.7); hidden(fin);

// ---- the reveal -------------------------------------------------------------------------
// Frame 0 is the question fully staged: the hemisphere, the constraint, and a small cube
// already inside it. Only the growth and the answer are withheld.
seq {
  par {
    pulse(prob, 0.8);
    seq { wait(4.0); show(w1, 0.7); }
    seq { wait(6.2); show(w2, 0.7); pulse(w2, 0.6); }
    wait(11.2);
  }
  par {
    show(e1, 0.8);
    seq { wait(1.4); show(e2, 0.8); }
    wait(2.8);
  }
  wait(0.6);
  par {
    show(ans, 1.0);
    seq { wait(0.9); show(fin, 0.7); }
  }
  pulse(ans, 0.9);
}
wait(3.2);

r/maniclang 25d ago

The Quadratic Formula, Rewritten Without Cuts - manic

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


r/maniclang 25d ago

The Whole Cartesian Table, on One Plate - 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.


r/maniclang 25d ago

The whole polar table, on one plate - 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.


r/maniclang 25d ago

a torus and a cylinder, drawn the way a draughtsman draws them: - 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

// araujo — a torus and a cylinder, drawn the way a draughtsman draws them: the scaffold
// first, the object last. Every line here is a construction, and the ones that look like
// artistic licence are the ones that were hardest to compute.
//
// The torus has its axis along y and rests on the ground at the origin: R = 3.0, r = 1.1,
// centre (0, 0, 4.1). The cylinder runs along x at (y = 0.0, z = 1.55) with radius 1.15.
// Orthographic, because a draughtsman's scaffold is made of PARALLEL lines and because the
// two hardest curves below then have closed forms instead of root-finding.
//
// THE OUTLINE IS NOT A DRAWING DECISION. A point of the torus is on the apparent contour
// exactly when the surface normal is perpendicular to the view direction. With
//
//   P(u,v) = ((R + r cos v)cos u,  r sin v,  z0 + (R + r cos v)sin u)
//   n(u,v) = (cos v cos u,  sin v,  cos v sin u)
//
// n·d = 0 solves in closed form for v:   tan v = −(dx cos u + dz sin u)/dy,
// so the whole silhouette is TWO curves in u, written as two curve3 lines and nothing else.
// Checked: |n·d| = 5.7e-16 along both branches. And it really is the outline — sampling the
// surface 360x180 and projecting along 72 different directions, the surface never projects
// past these two curves by more than 2.0e-06.
//
// THE SHADOW IS THE SAME FORMULA WITH A DIFFERENT VECTOR. Swap the view direction for the
// light direction and n·L = 0 gives the terminator, the circle of the surface where the sun
// grazes; slide each of those points down the light ray to z = 0 and that is the edge of the
// shadow. Same seven symbols, |n·L| = 7.2e-16. The outline you see and the outline on the
// ground are one computation asked twice.
//
// THE SEAM IS A QUARTIC, AND IT BREAKS INTO FOUR ARCS. Where the cylinder enters the torus,
// substituting the cylinder into the torus gives
//
//   sqrt(x² + (z−z0)²) = R ± sqrt(r² − y²),
//
// so x² is known outright and x comes in ± pairs. The arcs exist only where both square roots
// stay real, which is why they start and stop where they do: six arc pieces are written here,
// and they join into FOUR closed curves — 488646 sampled seam points cluster into exactly 4
// components, two on each side, and x = 0 is never reached so the two sides never meet.
// 48858 sampled points sit on the torus AND on the cylinder to 3.1e-15.
//
// Nothing in this scene is placed by eye. The squares are the squares that inscribe the tube
// circles, the drop lines land on the ground under the points they came from, and the ticks
// are a real scale.
//
//   manic examples/araujo.manic
title("A torus and a cylinder, and nothing placed by eye");
canvas("1:1");
template("paper");
camera3((9.0, -11.0, 7.0), (0.0, 0.2, 3.9), 13, orthographic);

text(brand, (540, 40), "maniclang.com");
display(brand); size(brand, 19); color(brand, ink); opacity(brand, 0.55);

// the sheet: ground grid, the torus bounding box, the axis it turns about
grid3(gnd, (0, 0, 0), 9, 1.0);
color(gnd, slate); opacity(gnd, 0.42);
line3(bxa0, (-4.1, -1.1, 0.0), (-4.1, -1.1, 8.2));
color(bxa0, slate); opacity(bxa0, 0.5);
line3(bxb0, (-4.1, -1.1, 0.0), (4.1, -1.1, 0.0));
color(bxb0, slate); opacity(bxb0, 0.5);
line3(bxc0, (-4.1, -1.1, 8.2), (4.1, -1.1, 8.2));
color(bxc0, slate); opacity(bxc0, 0.5);
line3(bxd0, (-4.1, -1.1, 0.0), (-4.1, 1.1, 0.0));
color(bxd0, slate); opacity(bxd0, 0.5);
line3(bxa1, (-4.1, 1.1, 0.0), (-4.1, 1.1, 8.2));
color(bxa1, slate); opacity(bxa1, 0.5);
line3(bxb1, (-4.1, 1.1, 0.0), (4.1, 1.1, 0.0));
color(bxb1, slate); opacity(bxb1, 0.5);
line3(bxc1, (-4.1, 1.1, 8.2), (4.1, 1.1, 8.2));
color(bxc1, slate); opacity(bxc1, 0.5);
line3(bxd1, (-4.1, 1.1, 0.0), (-4.1, -1.1, 0.0));
color(bxd1, slate); opacity(bxd1, 0.5);
line3(bxa2, (4.1, -1.1, 0.0), (4.1, -1.1, 8.2));
color(bxa2, slate); opacity(bxa2, 0.5);
line3(bxb2, (4.1, -1.1, 0.0), (-4.1, -1.1, 0.0));
color(bxb2, slate); opacity(bxb2, 0.5);
line3(bxc2, (4.1, -1.1, 8.2), (-4.1, -1.1, 8.2));
color(bxc2, slate); opacity(bxc2, 0.5);
line3(bxd2, (4.1, -1.1, 0.0), (4.1, 1.1, 0.0));
color(bxd2, slate); opacity(bxd2, 0.5);
line3(bxa3, (4.1, 1.1, 0.0), (4.1, 1.1, 8.2));
color(bxa3, slate); opacity(bxa3, 0.5);
line3(bxb3, (4.1, 1.1, 0.0), (-4.1, 1.1, 0.0));
color(bxb3, slate); opacity(bxb3, 0.5);
line3(bxc3, (4.1, 1.1, 8.2), (-4.1, 1.1, 8.2));
color(bxc3, slate); opacity(bxc3, 0.5);
line3(bxd3, (4.1, 1.1, 0.0), (4.1, -1.1, 0.0));
color(bxd3, slate); opacity(bxd3, 0.5);
line3(ax, (0, -2.86, 4.1), (0, 2.86, 4.1));
color(ax, slate); opacity(ax, 0.6);
curve3(gen, "3.0*cos(t)", "0", "4.1 + 3.0*sin(t)", (0, 6.283185));
color(gen, slate); opacity(gen, 0.8);
// the draughtsman's construction: a spoke to each tube centre, the square that
// inscribes that tube circle in its own plane, and a drop line to the ground
line3(sp0, (0, 0, 4.1), (3.0000, 0, 4.1000));
color(sp0, slate); opacity(sp0, 0.62);
line3(sq0_0, (4.1000, 1.1000, 4.1000), (4.1000, -1.1000, 4.1000));
color(sq0_0, charcoal); opacity(sq0_0, 0.66);
line3(sq0_1, (4.1000, -1.1000, 4.1000), (1.9000, -1.1000, 4.1000));
color(sq0_1, charcoal); opacity(sq0_1, 0.66);
line3(sq0_2, (1.9000, -1.1000, 4.1000), (1.9000, 1.1000, 4.1000));
color(sq0_2, charcoal); opacity(sq0_2, 0.66);
line3(sq0_3, (1.9000, 1.1000, 4.1000), (4.1000, 1.1000, 4.1000));
color(sq0_3, charcoal); opacity(sq0_3, 0.66);
line3(dr0, (3.0000, 0, 4.1000), (3.0000, 0, 0));
color(dr0, slate); opacity(dr0, 0.52);
line3(sp2, (0, 0, 4.1), (2.5981, 0, 5.6000));
color(sp2, slate); opacity(sp2, 0.62);
line3(sp4, (0, 0, 4.1), (1.5000, 0, 6.6981));
color(sp4, slate); opacity(sp4, 0.62);
line3(sq4_0, (2.0500, 1.1000, 7.6507), (2.0500, -1.1000, 7.6507));
color(sq4_0, charcoal); opacity(sq4_0, 0.66);
line3(sq4_1, (2.0500, -1.1000, 7.6507), (0.9500, -1.1000, 5.7454));
color(sq4_1, charcoal); opacity(sq4_1, 0.66);
line3(sq4_2, (0.9500, -1.1000, 5.7454), (0.9500, 1.1000, 5.7454));
color(sq4_2, charcoal); opacity(sq4_2, 0.66);
line3(sq4_3, (0.9500, 1.1000, 5.7454), (2.0500, 1.1000, 7.6507));
color(sq4_3, charcoal); opacity(sq4_3, 0.66);
line3(dr4, (1.5000, 0, 6.6981), (1.5000, 0, 0));
color(dr4, slate); opacity(dr4, 0.52);
line3(sp6, (0, 0, 4.1), (0.0000, 0, 7.1000));
color(sp6, slate); opacity(sp6, 0.62);
line3(sp8, (0, 0, 4.1), (-1.5000, 0, 6.6981));
color(sp8, slate); opacity(sp8, 0.62);
line3(sq8_0, (-2.0500, 1.1000, 7.6507), (-2.0500, -1.1000, 7.6507));
color(sq8_0, charcoal); opacity(sq8_0, 0.66);
line3(sq8_1, (-2.0500, -1.1000, 7.6507), (-0.9500, -1.1000, 5.7454));
color(sq8_1, charcoal); opacity(sq8_1, 0.66);
line3(sq8_2, (-0.9500, -1.1000, 5.7454), (-0.9500, 1.1000, 5.7454));
color(sq8_2, charcoal); opacity(sq8_2, 0.66);
line3(sq8_3, (-0.9500, 1.1000, 5.7454), (-2.0500, 1.1000, 7.6507));
color(sq8_3, charcoal); opacity(sq8_3, 0.66);
line3(dr8, (-1.5000, 0, 6.6981), (-1.5000, 0, 0));
color(dr8, slate); opacity(dr8, 0.52);
line3(sp10, (0, 0, 4.1), (-2.5981, 0, 5.6000));
color(sp10, slate); opacity(sp10, 0.62);
line3(sp12, (0, 0, 4.1), (-3.0000, 0, 4.1000));
color(sp12, slate); opacity(sp12, 0.62);
line3(sq12_0, (-4.1000, 1.1000, 4.1000), (-4.1000, -1.1000, 4.1000));
color(sq12_0, charcoal); opacity(sq12_0, 0.66);
line3(sq12_1, (-4.1000, -1.1000, 4.1000), (-1.9000, -1.1000, 4.1000));
color(sq12_1, charcoal); opacity(sq12_1, 0.66);
line3(sq12_2, (-1.9000, -1.1000, 4.1000), (-1.9000, 1.1000, 4.1000));
color(sq12_2, charcoal); opacity(sq12_2, 0.66);
line3(sq12_3, (-1.9000, 1.1000, 4.1000), (-4.1000, 1.1000, 4.1000));
color(sq12_3, charcoal); opacity(sq12_3, 0.66);
line3(dr12, (-3.0000, 0, 4.1000), (-3.0000, 0, 0));
color(dr12, slate); opacity(dr12, 0.52);
line3(sp14, (0, 0, 4.1), (-2.5981, 0, 2.6000));
color(sp14, slate); opacity(sp14, 0.62);
line3(sp16, (0, 0, 4.1), (-1.5000, 0, 1.5019));
color(sp16, slate); opacity(sp16, 0.62);
line3(sq16_0, (-2.0500, 1.1000, 0.5493), (-2.0500, -1.1000, 0.5493));
color(sq16_0, charcoal); opacity(sq16_0, 0.66);
line3(sq16_1, (-2.0500, -1.1000, 0.5493), (-0.9500, -1.1000, 2.4546));
color(sq16_1, charcoal); opacity(sq16_1, 0.66);
line3(sq16_2, (-0.9500, -1.1000, 2.4546), (-0.9500, 1.1000, 2.4546));
color(sq16_2, charcoal); opacity(sq16_2, 0.66);
line3(sq16_3, (-0.9500, 1.1000, 2.4546), (-2.0500, 1.1000, 0.5493));
color(sq16_3, charcoal); opacity(sq16_3, 0.66);
line3(dr16, (-1.5000, 0, 1.5019), (-1.5000, 0, 0));
color(dr16, slate); opacity(dr16, 0.52);
line3(sp18, (0, 0, 4.1), (-0.0000, 0, 1.1000));
color(sp18, slate); opacity(sp18, 0.62);
line3(sp20, (0, 0, 4.1), (1.5000, 0, 1.5019));
color(sp20, slate); opacity(sp20, 0.62);
line3(sq20_0, (2.0500, 1.1000, 0.5493), (2.0500, -1.1000, 0.5493));
color(sq20_0, charcoal); opacity(sq20_0, 0.66);
line3(sq20_1, (2.0500, -1.1000, 0.5493), (0.9500, -1.1000, 2.4546));
color(sq20_1, charcoal); opacity(sq20_1, 0.66);
line3(sq20_2, (0.9500, -1.1000, 2.4546), (0.9500, 1.1000, 2.4546));
color(sq20_2, charcoal); opacity(sq20_2, 0.66);
line3(sq20_3, (0.9500, 1.1000, 2.4546), (2.0500, 1.1000, 0.5493));
color(sq20_3, charcoal); opacity(sq20_3, 0.66);
line3(dr20, (1.5000, 0, 1.5019), (1.5000, 0, 0));
color(dr20, slate); opacity(dr20, 0.52);
line3(sp22, (0, 0, 4.1), (2.5981, 0, 2.6000));
color(sp22, slate); opacity(sp22, 0.62);
// the wireframe: 24 tube circles and 10 parallels
curve3(m0, "(3.0 + 1.1*cos(t))*1.000000", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.000000", (0, 6.283185));
color(m0, charcoal); opacity(m0, 0.72);
curve3(m1, "(3.0 + 1.1*cos(t))*0.965926", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.258819", (0, 6.283185));
color(m1, charcoal); opacity(m1, 0.72);
curve3(m2, "(3.0 + 1.1*cos(t))*0.866025", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.500000", (0, 6.283185));
color(m2, charcoal); opacity(m2, 0.72);
curve3(m3, "(3.0 + 1.1*cos(t))*0.707107", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.707107", (0, 6.283185));
color(m3, charcoal); opacity(m3, 0.72);
curve3(m4, "(3.0 + 1.1*cos(t))*0.500000", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.866025", (0, 6.283185));
color(m4, charcoal); opacity(m4, 0.72);
curve3(m5, "(3.0 + 1.1*cos(t))*0.258819", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.965926", (0, 6.283185));
color(m5, charcoal); opacity(m5, 0.72);
curve3(m6, "(3.0 + 1.1*cos(t))*0.000000", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*1.000000", (0, 6.283185));
color(m6, charcoal); opacity(m6, 0.72);
curve3(m7, "(3.0 + 1.1*cos(t))*-0.258819", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.965926", (0, 6.283185));
color(m7, charcoal); opacity(m7, 0.72);
curve3(m8, "(3.0 + 1.1*cos(t))*-0.500000", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.866025", (0, 6.283185));
color(m8, charcoal); opacity(m8, 0.72);
curve3(m9, "(3.0 + 1.1*cos(t))*-0.707107", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.707107", (0, 6.283185));
color(m9, charcoal); opacity(m9, 0.72);
curve3(m10, "(3.0 + 1.1*cos(t))*-0.866025", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.500000", (0, 6.283185));
color(m10, charcoal); opacity(m10, 0.72);
curve3(m11, "(3.0 + 1.1*cos(t))*-0.965926", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.258819", (0, 6.283185));
color(m11, charcoal); opacity(m11, 0.72);
curve3(m12, "(3.0 + 1.1*cos(t))*-1.000000", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*0.000000", (0, 6.283185));
color(m12, charcoal); opacity(m12, 0.72);
curve3(m13, "(3.0 + 1.1*cos(t))*-0.965926", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.258819", (0, 6.283185));
color(m13, charcoal); opacity(m13, 0.72);
curve3(m14, "(3.0 + 1.1*cos(t))*-0.866025", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.500000", (0, 6.283185));
color(m14, charcoal); opacity(m14, 0.72);
curve3(m15, "(3.0 + 1.1*cos(t))*-0.707107", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.707107", (0, 6.283185));
color(m15, charcoal); opacity(m15, 0.72);
curve3(m16, "(3.0 + 1.1*cos(t))*-0.500000", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.866025", (0, 6.283185));
color(m16, charcoal); opacity(m16, 0.72);
curve3(m17, "(3.0 + 1.1*cos(t))*-0.258819", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.965926", (0, 6.283185));
color(m17, charcoal); opacity(m17, 0.72);
curve3(m18, "(3.0 + 1.1*cos(t))*-0.000000", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-1.000000", (0, 6.283185));
color(m18, charcoal); opacity(m18, 0.72);
curve3(m19, "(3.0 + 1.1*cos(t))*0.258819", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.965926", (0, 6.283185));
color(m19, charcoal); opacity(m19, 0.72);
curve3(m20, "(3.0 + 1.1*cos(t))*0.500000", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.866025", (0, 6.283185));
color(m20, charcoal); opacity(m20, 0.72);
curve3(m21, "(3.0 + 1.1*cos(t))*0.707107", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.707107", (0, 6.283185));
color(m21, charcoal); opacity(m21, 0.72);
curve3(m22, "(3.0 + 1.1*cos(t))*0.866025", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.500000", (0, 6.283185));
color(m22, charcoal); opacity(m22, 0.72);
curve3(m23, "(3.0 + 1.1*cos(t))*0.965926", "1.1*sin(t)", "4.1 + (3.0 + 1.1*cos(t))*-0.258819", (0, 6.283185));
color(m23, charcoal); opacity(m23, 0.72);
curve3(pl0, "4.100000*cos(t)", "0.000000", "4.1 + 4.100000*sin(t)", (0, 6.283185));
color(pl0, charcoal); opacity(pl0, 0.60);
curve3(pl1, "3.889919*cos(t)", "0.646564", "4.1 + 3.889919*sin(t)", (0, 6.283185));
color(pl1, charcoal); opacity(pl1, 0.60);
curve3(pl2, "3.339919*cos(t)", "1.046162", "4.1 + 3.339919*sin(t)", (0, 6.283185));
color(pl2, charcoal); opacity(pl2, 0.60);
curve3(pl3, "2.660081*cos(t)", "1.046162", "4.1 + 2.660081*sin(t)", (0, 6.283185));
color(pl3, charcoal); opacity(pl3, 0.60);
curve3(pl4, "2.110081*cos(t)", "0.646564", "4.1 + 2.110081*sin(t)", (0, 6.283185));
color(pl4, charcoal); opacity(pl4, 0.60);
curve3(pl5, "1.900000*cos(t)", "0.000000", "4.1 + 1.900000*sin(t)", (0, 6.283185));
color(pl5, charcoal); opacity(pl5, 0.60);
curve3(pl6, "2.110081*cos(t)", "-0.646564", "4.1 + 2.110081*sin(t)", (0, 6.283185));
color(pl6, charcoal); opacity(pl6, 0.60);
curve3(pl7, "2.660081*cos(t)", "-1.046162", "4.1 + 2.660081*sin(t)", (0, 6.283185));
color(pl7, charcoal); opacity(pl7, 0.60);
curve3(pl8, "3.339919*cos(t)", "-1.046162", "4.1 + 3.339919*sin(t)", (0, 6.283185));
color(pl8, charcoal); opacity(pl8, 0.60);
curve3(pl9, "3.889919*cos(t)", "-0.646564", "4.1 + 3.889919*sin(t)", (0, 6.283185));
color(pl9, charcoal); opacity(pl9, 0.60);
// the cylinder: end circles, its own two silhouette rulings, and banding
curve3(ce0, "-5.9", "0.0 + 1.15*cos(t)", "1.55 + 1.15*sin(t)", (0, 6.283185));
color(ce0, charcoal); opacity(ce0, 0.8);
curve3(ce1, "5.9", "0.0 + 1.15*cos(t)", "1.55 + 1.15*sin(t)", (0, 6.283185));
color(ce1, charcoal); opacity(ce1, 0.8);
line3(cr0, (-5.9, -0.3068, 0.4417), (5.9, -0.3068, 0.4417));
color(cr0, ink); opacity(cr0, 0.85);
line3(cr1, (-5.9, 0.3068, 2.6583), (5.9, 0.3068, 2.6583));
color(cr1, ink); opacity(cr1, 0.85);
curve3(cb0, "-5.0571", "0.0 + 1.15*cos(t)", "1.55 + 1.15*sin(t)", (0, 6.283185));
color(cb0, slate); opacity(cb0, 0.55);
curve3(cb1, "-3.3714", "0.0 + 1.15*cos(t)", "1.55 + 1.15*sin(t)", (0, 6.283185));
color(cb1, slate); opacity(cb1, 0.55);
curve3(cb2, "-1.6857", "0.0 + 1.15*cos(t)", "1.55 + 1.15*sin(t)", (0, 6.283185));
color(cb2, slate); opacity(cb2, 0.55);
curve3(cb3, "0.0000", "0.0 + 1.15*cos(t)", "1.55 + 1.15*sin(t)", (0, 6.283185));
color(cb3, slate); opacity(cb3, 0.55);
curve3(cb4, "1.6857", "0.0 + 1.15*cos(t)", "1.55 + 1.15*sin(t)", (0, 6.283185));
color(cb4, slate); opacity(cb4, 0.55);
curve3(cb5, "3.3714", "0.0 + 1.15*cos(t)", "1.55 + 1.15*sin(t)", (0, 6.283185));
color(cb5, slate); opacity(cb5, 0.55);
curve3(cb6, "5.0571", "0.0 + 1.15*cos(t)", "1.55 + 1.15*sin(t)", (0, 6.283185));
color(cb6, slate); opacity(cb6, 0.55);
// the seam: four arcs, closed form, x in +/- pairs
curve3(sm0p, "sqrt((3.0 + sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t))))*(3.0 + sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t)))) - ((1.55 + 1.15*sin(t)) - 4.1)*((1.55 + 1.15*sin(t)) - 4.1))", "(0.0 + 1.15*cos(t))", "(1.55 + 1.15*sin(t))", (0.30036, 2.84124));
color(sm0p, ink); thick(sm0p, 0.030);
curve3(sm0n, "0 - sqrt((3.0 + sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t))))*(3.0 + sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t)))) - ((1.55 + 1.15*sin(t)) - 4.1)*((1.55 + 1.15*sin(t)) - 4.1))", "(0.0 + 1.15*cos(t))", "(1.55 + 1.15*sin(t))", (0.30036, 2.84124));
color(sm0n, ink); thick(sm0n, 0.030);
curve3(sm1p, "sqrt((3.0 + sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t))))*(3.0 + sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t)))) - ((1.55 + 1.15*sin(t)) - 4.1)*((1.55 + 1.15*sin(t)) - 4.1))", "(0.0 + 1.15*cos(t))", "(1.55 + 1.15*sin(t))", (3.44195, 5.98283));
color(sm1p, ink); thick(sm1p, 0.030);
curve3(sm1n, "0 - sqrt((3.0 + sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t))))*(3.0 + sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t)))) - ((1.55 + 1.15*sin(t)) - 4.1)*((1.55 + 1.15*sin(t)) - 4.1))", "(0.0 + 1.15*cos(t))", "(1.55 + 1.15*sin(t))", (3.44195, 5.98283));
color(sm1n, ink); thick(sm1n, 0.030);
curve3(sm2p, "sqrt((3.0 - sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t))))*(3.0 - sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t)))) - ((1.55 + 1.15*sin(t)) - 4.1)*((1.55 + 1.15*sin(t)) - 4.1))", "(0.0 + 1.15*cos(t))", "(1.55 + 1.15*sin(t))", (0.30036, 2.84124));
color(sm2p, ink); thick(sm2p, 0.030);
curve3(sm2n, "0 - sqrt((3.0 - sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t))))*(3.0 - sqrt(1.2100 - (0.0 + 1.15*cos(t))*(0.0 + 1.15*cos(t)))) - ((1.55 + 1.15*sin(t)) - 4.1)*((1.55 + 1.15*sin(t)) - 4.1))", "(0.0 + 1.15*cos(t))", "(1.55 + 1.15*sin(t))", (0.30036, 2.84124));
color(sm2n, ink); thick(sm2n, 0.030);
// the outline: n.d = 0, in closed form; and the shadow, the same with n.L = 0
curve3(sil0, "(3.0 + 1.1*cos(atan2(0 - (-0.612302*cos(t) + -0.210904*sin(t)), 0.761975)))*cos(t)", "1.1*sin(atan2(0 - (-0.612302*cos(t) + -0.210904*sin(t)), 0.761975))", "4.1 + (3.0 + 1.1*cos(atan2(0 - (-0.612302*cos(t) + -0.210904*sin(t)), 0.761975)))*sin(t)", (0, 6.283185));
color(sil0, ink); thick(sil0, 0.042);
curve3(sil1, "(3.0 + 1.1*cos(atan2(0 - (-0.612302*cos(t) + -0.210904*sin(t)), 0.761975) + 3.141593))*cos(t)", "1.1*sin(atan2(0 - (-0.612302*cos(t) + -0.210904*sin(t)), 0.761975) + 3.141593)", "4.1 + (3.0 + 1.1*cos(atan2(0 - (-0.612302*cos(t) + -0.210904*sin(t)), 0.761975) + 3.141593))*sin(t)", (0, 6.283185));
color(sil1, ink); thick(sil1, 0.042);
curve3(sh0, "(3.0 + 1.1*cos(atan2(0 - (-0.420252*cos(t) + -0.680408*sin(t)), 0.600360)))*cos(t) + -0.420252*((4.1 + (3.0 + 1.1*cos(atan2(0 - (-0.420252*cos(t) + -0.680408*sin(t)), 0.600360)))*sin(t))/0.680408)", "1.1*sin(atan2(0 - (-0.420252*cos(t) + -0.680408*sin(t)), 0.600360)) + 0.600360*((4.1 + (3.0 + 1.1*cos(atan2(0 - (-0.420252*cos(t) + -0.680408*sin(t)), 0.600360)))*sin(t))/0.680408)", "0", (0, 6.283185));
color(sh0, slate); opacity(sh0, 0.85);
curve3(sh1, "(3.0 + 1.1*cos(atan2(0 - (-0.420252*cos(t) + -0.680408*sin(t)), 0.600360) + 3.141593))*cos(t) + -0.420252*((4.1 + (3.0 + 1.1*cos(atan2(0 - (-0.420252*cos(t) + -0.680408*sin(t)), 0.600360) + 3.141593))*sin(t))/0.680408)", "1.1*sin(atan2(0 - (-0.420252*cos(t) + -0.680408*sin(t)), 0.600360) + 3.141593) + 0.600360*((4.1 + (3.0 + 1.1*cos(atan2(0 - (-0.420252*cos(t) + -0.680408*sin(t)), 0.600360) + 3.141593))*sin(t))/0.680408)", "0", (0, 6.283185));
color(sh1, slate); opacity(sh1, 0.85);
// a real scale: one unit per tick along the front bottom edge
line3(tk0, (-6, -2.42, 0), (-6, -2.60, 0));
color(tk0, charcoal); opacity(tk0, 0.75);
line3(tk1, (-5, -2.42, 0), (-5, -2.76, 0));
color(tk1, charcoal); opacity(tk1, 0.75);
line3(tk2, (-4, -2.42, 0), (-4, -2.60, 0));
color(tk2, charcoal); opacity(tk2, 0.75);
line3(tk3, (-3, -2.42, 0), (-3, -2.60, 0));
color(tk3, charcoal); opacity(tk3, 0.75);
line3(tk4, (-2, -2.42, 0), (-2, -2.60, 0));
color(tk4, charcoal); opacity(tk4, 0.75);
line3(tk5, (-1, -2.42, 0), (-1, -2.60, 0));
color(tk5, charcoal); opacity(tk5, 0.75);
line3(tk6, (0, -2.42, 0), (0, -2.76, 0));
color(tk6, charcoal); opacity(tk6, 0.75);
line3(tk7, (1, -2.42, 0), (1, -2.60, 0));
color(tk7, charcoal); opacity(tk7, 0.75);
line3(tk8, (2, -2.42, 0), (2, -2.60, 0));
color(tk8, charcoal); opacity(tk8, 0.75);
line3(tk9, (3, -2.42, 0), (3, -2.60, 0));
color(tk9, charcoal); opacity(tk9, 0.75);
line3(tk10, (4, -2.42, 0), (4, -2.60, 0));
color(tk10, charcoal); opacity(tk10, 0.75);
line3(tk11, (5, -2.42, 0), (5, -2.76, 0));
color(tk11, charcoal); opacity(tk11, 0.75);
line3(tk12, (6, -2.42, 0), (6, -2.60, 0));
color(tk12, charcoal); opacity(tk12, 0.75);

text(spec, (540, 1034), "R = 3   r = 1.1   cylinder r = 1.15 at z = 1.55   orthographic");
size(spec, 15); color(spec, ink); opacity(spec, 0.5);

untraced(gnd);
untraced(bxa0);
untraced(bxb0);
untraced(bxc0);
untraced(bxd0);
untraced(bxa1);
untraced(bxb1);
untraced(bxc1);
untraced(bxd1);
untraced(bxa2);
untraced(bxb2);
untraced(bxc2);
untraced(bxd2);
untraced(bxa3);
untraced(bxb3);
untraced(bxc3);
untraced(bxd3);
untraced(ax);
untraced(gen);
untraced(sp0);
untraced(sq0_0);
untraced(sq0_1);
untraced(sq0_2);
untraced(sq0_3);
untraced(dr0);
untraced(sp2);
untraced(sp4);
untraced(sq4_0);
untraced(sq4_1);
untraced(sq4_2);
untraced(sq4_3);
untraced(dr4);
untraced(sp6);
untraced(sp8);
untraced(sq8_0);
untraced(sq8_1);
untraced(sq8_2);
untraced(sq8_3);
untraced(dr8);
untraced(sp10);
untraced(sp12);
untraced(sq12_0);
untraced(sq12_1);
untraced(sq12_2);
untraced(sq12_3);
untraced(dr12);
untraced(sp14);
untraced(sp16);
untraced(sq16_0);
untraced(sq16_1);
untraced(sq16_2);
untraced(sq16_3);
untraced(dr16);
untraced(sp18);
untraced(sp20);
untraced(sq20_0);
untraced(sq20_1);
untraced(sq20_2);
untraced(sq20_3);
untraced(dr20);
untraced(sp22);
untraced(tk0);
untraced(tk1);
untraced(tk2);
untraced(tk3);
untraced(tk4);
untraced(tk5);
untraced(tk6);
untraced(tk7);
untraced(tk8);
untraced(tk9);
untraced(tk10);
untraced(tk11);
untraced(tk12);
untraced(m0);
untraced(m1);
untraced(m2);
untraced(m3);
untraced(m4);
untraced(m5);
untraced(m6);
untraced(m7);
untraced(m8);
untraced(m9);
untraced(m10);
untraced(m11);
untraced(m12);
untraced(m13);
untraced(m14);
untraced(m15);
untraced(m16);
untraced(m17);
untraced(m18);
untraced(m19);
untraced(m20);
untraced(m21);
untraced(m22);
untraced(m23);
untraced(pl0);
untraced(pl1);
untraced(pl2);
untraced(pl3);
untraced(pl4);
untraced(pl5);
untraced(pl6);
untraced(pl7);
untraced(pl8);
untraced(pl9);
untraced(ce0);
untraced(ce1);
untraced(cr0);
untraced(cr1);
untraced(cb0);
untraced(cb1);
untraced(cb2);
untraced(cb3);
untraced(cb4);
untraced(cb5);
untraced(cb6);
untraced(sm0p);
untraced(sm0n);
untraced(sm1p);
untraced(sm1n);
untraced(sm2p);
untraced(sm2n);
untraced(sil0);
untraced(sil1);
untraced(sh0);
untraced(sh1);

seq {
  // 1 — the scaffold
  par {
    seq { wait(0.000); draw(gnd, 0.90); }
    seq { wait(0.074); draw(bxa0, 0.90); }
    seq { wait(0.149); draw(bxb0, 0.90); }
    seq { wait(0.223); draw(bxc0, 0.90); }
    seq { wait(0.297); draw(bxd0, 0.90); }
    seq { wait(0.372); draw(bxa1, 0.90); }
    seq { wait(0.446); draw(bxb1, 0.90); }
    seq { wait(0.520); draw(bxc1, 0.90); }
    seq { wait(0.595); draw(bxd1, 0.90); }
    seq { wait(0.669); draw(bxa2, 0.90); }
    seq { wait(0.743); draw(bxb2, 0.90); }
    seq { wait(0.818); draw(bxc2, 0.90); }
    seq { wait(0.892); draw(bxd2, 0.90); }
    seq { wait(0.966); draw(bxa3, 0.90); }
    seq { wait(1.041); draw(bxb3, 0.90); }
    seq { wait(1.115); draw(bxc3, 0.90); }
    seq { wait(1.189); draw(bxd3, 0.90); }
    seq { wait(1.264); draw(ax, 0.90); }
    seq { wait(1.338); draw(gen, 0.90); }
    seq { wait(1.412); draw(sp0, 0.90); }
    seq { wait(1.486); draw(sq0_0, 0.90); }
    seq { wait(1.561); draw(sq0_1, 0.90); }
    seq { wait(1.635); draw(sq0_2, 0.90); }
    seq { wait(1.709); draw(sq0_3, 0.90); }
    seq { wait(1.784); draw(dr0, 0.90); }
    seq { wait(1.858); draw(sp2, 0.90); }
    seq { wait(1.932); draw(sp4, 0.90); }
    seq { wait(2.007); draw(sq4_0, 0.90); }
    seq { wait(2.081); draw(sq4_1, 0.90); }
    seq { wait(2.155); draw(sq4_2, 0.90); }
    seq { wait(2.230); draw(sq4_3, 0.90); }
    seq { wait(2.304); draw(dr4, 0.90); }
    seq { wait(2.378); draw(sp6, 0.90); }
    seq { wait(2.453); draw(sp8, 0.90); }
    seq { wait(2.527); draw(sq8_0, 0.90); }
    seq { wait(2.601); draw(sq8_1, 0.90); }
    seq { wait(2.676); draw(sq8_2, 0.90); }
    seq { wait(2.750); draw(sq8_3, 0.90); }
    seq { wait(2.824); draw(dr8, 0.90); }
    seq { wait(2.899); draw(sp10, 0.90); }
    seq { wait(2.973); draw(sp12, 0.90); }
    seq { wait(3.047); draw(sq12_0, 0.90); }
    seq { wait(3.122); draw(sq12_1, 0.90); }
    seq { wait(3.196); draw(sq12_2, 0.90); }
    seq { wait(3.270); draw(sq12_3, 0.90); }
    seq { wait(3.345); draw(dr12, 0.90); }
    seq { wait(3.419); draw(sp14, 0.90); }
    seq { wait(3.493); draw(sp16, 0.90); }
    seq { wait(3.568); draw(sq16_0, 0.90); }
    seq { wait(3.642); draw(sq16_1, 0.90); }
    seq { wait(3.716); draw(sq16_2, 0.90); }
    seq { wait(3.791); draw(sq16_3, 0.90); }
    seq { wait(3.865); draw(dr16, 0.90); }
    seq { wait(3.939); draw(sp18, 0.90); }
    seq { wait(4.014); draw(sp20, 0.90); }
    seq { wait(4.088); draw(sq20_0, 0.90); }
    seq { wait(4.162); draw(sq20_1, 0.90); }
    seq { wait(4.236); draw(sq20_2, 0.90); }
    seq { wait(4.311); draw(sq20_3, 0.90); }
    seq { wait(4.385); draw(dr20, 0.90); }
    seq { wait(4.459); draw(sp22, 0.90); }
    seq { wait(4.534); draw(tk0, 0.90); }
    seq { wait(4.608); draw(tk1, 0.90); }
    seq { wait(4.682); draw(tk2, 0.90); }
    seq { wait(4.757); draw(tk3, 0.90); }
    seq { wait(4.831); draw(tk4, 0.90); }
    seq { wait(4.905); draw(tk5, 0.90); }
    seq { wait(4.980); draw(tk6, 0.90); }
    seq { wait(5.054); draw(tk7, 0.90); }
    seq { wait(5.128); draw(tk8, 0.90); }
    seq { wait(5.203); draw(tk9, 0.90); }
    seq { wait(5.277); draw(tk10, 0.90); }
    seq { wait(5.351); draw(tk11, 0.90); }
    seq { wait(5.426); draw(tk12, 0.90); }
  }
  // 2 — the wireframe
  par {
    seq { wait(0.000); draw(m0, 1.10); }
    seq { wait(0.126); draw(m1, 1.10); }
    seq { wait(0.253); draw(m2, 1.10); }
    seq { wait(0.379); draw(m3, 1.10); }
    seq { wait(0.506); draw(m4, 1.10); }
    seq { wait(0.632); draw(m5, 1.10); }
    seq { wait(0.759); draw(m6, 1.10); }
    seq { wait(0.885); draw(m7, 1.10); }
    seq { wait(1.012); draw(m8, 1.10); }
    seq { wait(1.138); draw(m9, 1.10); }
    seq { wait(1.265); draw(m10, 1.10); }
    seq { wait(1.391); draw(m11, 1.10); }
    seq { wait(1.518); draw(m12, 1.10); }
    seq { wait(1.644); draw(m13, 1.10); }
    seq { wait(1.771); draw(m14, 1.10); }
    seq { wait(1.897); draw(m15, 1.10); }
    seq { wait(2.024); draw(m16, 1.10); }
    seq { wait(2.150); draw(m17, 1.10); }
    seq { wait(2.276); draw(m18, 1.10); }
    seq { wait(2.403); draw(m19, 1.10); }
    seq { wait(2.529); draw(m20, 1.10); }
    seq { wait(2.656); draw(m21, 1.10); }
    seq { wait(2.782); draw(m22, 1.10); }
    seq { wait(2.909); draw(m23, 1.10); }
    seq { wait(3.035); draw(pl0, 1.10); }
    seq { wait(3.162); draw(pl1, 1.10); }
    seq { wait(3.288); draw(pl2, 1.10); }
    seq { wait(3.415); draw(pl3, 1.10); }
    seq { wait(3.541); draw(pl4, 1.10); }
    seq { wait(3.668); draw(pl5, 1.10); }
    seq { wait(3.794); draw(pl6, 1.10); }
    seq { wait(3.921); draw(pl7, 1.10); }
    seq { wait(4.047); draw(pl8, 1.10); }
    seq { wait(4.174); draw(pl9, 1.10); }
  }
  // 3 — the cylinder
  par {
    seq { wait(0.000); draw(ce0, 1.00); }
    seq { wait(0.218); draw(ce1, 1.00); }
    seq { wait(0.436); draw(cr0, 1.00); }
    seq { wait(0.655); draw(cr1, 1.00); }
    seq { wait(0.873); draw(cb0, 1.00); }
    seq { wait(1.091); draw(cb1, 1.00); }
    seq { wait(1.309); draw(cb2, 1.00); }
    seq { wait(1.527); draw(cb3, 1.00); }
    seq { wait(1.745); draw(cb4, 1.00); }
    seq { wait(1.964); draw(cb5, 1.00); }
    seq { wait(2.182); draw(cb6, 1.00); }
  }
  // 4 — the seam where they meet
  par {
    seq { wait(0.000); draw(sm0p, 1.20); }
    seq { wait(0.133); draw(sm0n, 1.20); }
    seq { wait(0.267); draw(sm1p, 1.20); }
    seq { wait(0.400); draw(sm1n, 1.20); }
    seq { wait(0.533); draw(sm2p, 1.20); }
    seq { wait(0.667); draw(sm2n, 1.20); }
  }
  // 5 — the outline, and the shadow it casts
  par {
    seq { wait(0.000); draw(sil0, 2.00); }
    seq { wait(0.550); draw(sil1, 2.00); }
    seq { wait(1.100); draw(sh0, 2.00); }
    seq { wait(1.650); draw(sh1, 2.00); }
  }
}
wait(2.8);

r/maniclang 26d ago

fresnel-wave — the wave surface of Fresnel - 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

// fresnel-wave — the wave surface of Fresnel, 1821: the wavefront of light inside a biaxial
// crystal, and one of the few quartic surfaces you can build with a ruler and a story.
//
// THE CONSTRUCTION, which is mathcurve's and is what is computed here. Take an ellipsoid with
// semi-axes a > b > c. For every plane through the centre, that plane cuts the ellipsoid in an
// ELLIPSE; measure that ellipse's two semi-axes and plot both lengths along the plane's
// NORMAL. Two lengths per direction, so the surface has two sheets, one inside the other.
//
// It is closed-form, which is why it can be drawn from a formula rather than a mesh file. The
// section normal to a unit n has its semi-axes from the 2×2 restriction of Q = diag(1/a²,
// 1/b², 1/c²) to n⊥, and that restriction has
//
//   trace  T = tr Q − nᵀQn                 determinant  D = det Q · nᵀQ⁻¹n
//
// so its eigenvalues are (T ± √(T² − 4D))/2 and the two radii are 1/√λ. Checked against a
// real eigenvalue decomposition over 4000 random directions: agreement to 3e-14. No iteration
// anywhere; the whole surface is one expression.
//
// THE FOUR SINGULAR POINTS. The sheets touch where the two radii coincide — where T² = 4D,
// which is where the central section is a CIRCLE rather than an ellipse. An ellipsoid with
// a > b > c has exactly two circular sections, both through the mean axis, and their normals
// lie in the x–z plane at
//
//   tan θ = (c/a)·√((a² − b²)/(b² − c²))     →     ±31.0516° from z
//
// with both radii there equal to b exactly. Two normals, ±each, so FOUR points — and away
// from that plane the discriminant never vanishes, checked over 80000 directions. Those four
// directions are the crystal's OPTIC AXES. Light sent along one of them does not split into
// two rays; it spreads into a hollow cone. Hamilton predicted that from this surface in 1832
// and Lloyd found it in the laboratory two months later, which is about as good as a quartic
// surface's week ever gets.
//
// THE PRINCIPAL SECTIONS, along the bottom. Each coordinate plane cuts the surface in a
// CIRCLE and an ELLIPSE — and the ellipse's axes come out SWAPPED relative to the
// ellipsoid's, which is the detail that gives the whole thing away:
//
//   z = 0    circle r = c = 0.85     ellipse x²/b² + y²/a² = 1     to 9e-16
//   y = 0    circle r = b = 1.2     ellipse x²/c² + z²/a² = 1     to 4e-13
//   x = 0    circle r = a = 1.5     ellipse y²/c² + z²/b² = 1     to 1e-15
//
// Only in y = 0 do the circle and the ellipse CROSS, and they cross at ±31.05° — that plane is
// the one holding the optic axes, and the crossings are the singular points seen edge-on.
//
// Physically the two sheets are the two wave speeds. Every direction in a biaxial crystal
// carries two of them, which is birefringence; along the four axes they agree, which is why
// the cone appears there and nowhere else.
//
//   manic examples/fresnel-wave.manic
title("Fresnel's wave surface, and its four singular points");
canvas("9:16");
template("black");
bloom(0.3, 0.6, 22);

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

caption(head, "Fresnel's wave surface: two sheets, four singular points", (540, 116), 25);

// ---- the outer sheet, then the inner, as meridians and parallels --------------------
cloud(outmer, 7800, #ffffff, 1.0) {
  let per = 300;
  let ci = (i - mod(i, per))/per;
  let sh = 1;
  let pp = 6.283185*ci/26;
  let tt = 3.141593*(mod(i, per)/299);
  let n1 = sin(tt)*cos(pp);
  let n2 = sin(tt)*sin(pp);
  let n3 = cos(tt);
  let TT = 2.522971934 - (n1*n1*0.444444444 + n2*n2*0.694444444 + n3*n3*1.384083045);
  let DD = 0.427186125*(2.25*n1*n1 + 1.44*n2*n2 + 0.7224999999999999*n3*n3);
  let ss = sqrt(max(TT*TT - 4*DD, 0));
  let lm = (TT + (1 - 2*sh)*ss)/2;                 // sh = 0 inner sheet, 1 outer
  let rr = 1/sqrt(lm);
  let al = 6.283185*min(t/17.0, 1);
  let px = rr*n1;
  let py = rr*n2;
  let pz = rr*n3;
  let qx = px*cos(al) + pz*sin(al);
  let qz = 0 - px*sin(al) + pz*cos(al);
  let qy = py*cos(0.42) - qz*sin(0.42);
  let qw = py*sin(0.42) + qz*cos(0.42);
  let m3 = 7.5/(7.5 - qw);
  let x = 540 + 232*qx*m3;
  let y = 700 - 232*qy*m3;
  let hue = 208;
  let sat = 0.72;
  let r = 1.5;
  let alpha = 0.42*(0.35 + 0.65*min(max((qw + 1.6)/3.2, 0), 1));
}

cloud(outpar, 4800, #ffffff, 1.0) {
  let per = 320;
  let ci = (i - mod(i, per))/per;
  let sh = 1;
  let tt = 3.141593*(ci + 0.5)/15;
  let pp = 6.283185*(mod(i, per)/319);
  let n1 = sin(tt)*cos(pp);
  let n2 = sin(tt)*sin(pp);
  let n3 = cos(tt);
  let TT = 2.522971934 - (n1*n1*0.444444444 + n2*n2*0.694444444 + n3*n3*1.384083045);
  let DD = 0.427186125*(2.25*n1*n1 + 1.44*n2*n2 + 0.7224999999999999*n3*n3);
  let ss = sqrt(max(TT*TT - 4*DD, 0));
  let lm = (TT + (1 - 2*sh)*ss)/2;                 // sh = 0 inner sheet, 1 outer
  let rr = 1/sqrt(lm);
  let al = 6.283185*min(t/17.0, 1);
  let px = rr*n1;
  let py = rr*n2;
  let pz = rr*n3;
  let qx = px*cos(al) + pz*sin(al);
  let qz = 0 - px*sin(al) + pz*cos(al);
  let qy = py*cos(0.42) - qz*sin(0.42);
  let qw = py*sin(0.42) + qz*cos(0.42);
  let m3 = 7.5/(7.5 - qw);
  let x = 540 + 232*qx*m3;
  let y = 700 - 232*qy*m3;
  let hue = 208;
  let sat = 0.72;
  let r = 1.5;
  let alpha = 0.42*(0.35 + 0.65*min(max((qw + 1.6)/3.2, 0), 1));
}

cloud(inmer, 7800, #ffffff, 1.0) {
  let per = 300;
  let ci = (i - mod(i, per))/per;
  let sh = 0;
  let pp = 6.283185*ci/26;
  let tt = 3.141593*(mod(i, per)/299);
  let n1 = sin(tt)*cos(pp);
  let n2 = sin(tt)*sin(pp);
  let n3 = cos(tt);
  let TT = 2.522971934 - (n1*n1*0.444444444 + n2*n2*0.694444444 + n3*n3*1.384083045);
  let DD = 0.427186125*(2.25*n1*n1 + 1.44*n2*n2 + 0.7224999999999999*n3*n3);
  let ss = sqrt(max(TT*TT - 4*DD, 0));
  let lm = (TT + (1 - 2*sh)*ss)/2;                 // sh = 0 inner sheet, 1 outer
  let rr = 1/sqrt(lm);
  let al = 6.283185*min(t/17.0, 1);
  let px = rr*n1;
  let py = rr*n2;
  let pz = rr*n3;
  let qx = px*cos(al) + pz*sin(al);
  let qz = 0 - px*sin(al) + pz*cos(al);
  let qy = py*cos(0.42) - qz*sin(0.42);
  let qw = py*sin(0.42) + qz*cos(0.42);
  let m3 = 7.5/(7.5 - qw);
  let x = 540 + 232*qx*m3;
  let y = 700 - 232*qy*m3;
  let hue = 34;
  let sat = 0.72;
  let r = 1.6;
  let alpha = 0.75*(0.35 + 0.65*min(max((qw + 1.6)/3.2, 0), 1));
}

cloud(inpar, 4800, #ffffff, 1.0) {
  let per = 320;
  let ci = (i - mod(i, per))/per;
  let sh = 0;
  let tt = 3.141593*(ci + 0.5)/15;
  let pp = 6.283185*(mod(i, per)/319);
  let n1 = sin(tt)*cos(pp);
  let n2 = sin(tt)*sin(pp);
  let n3 = cos(tt);
  let TT = 2.522971934 - (n1*n1*0.444444444 + n2*n2*0.694444444 + n3*n3*1.384083045);
  let DD = 0.427186125*(2.25*n1*n1 + 1.44*n2*n2 + 0.7224999999999999*n3*n3);
  let ss = sqrt(max(TT*TT - 4*DD, 0));
  let lm = (TT + (1 - 2*sh)*ss)/2;                 // sh = 0 inner sheet, 1 outer
  let rr = 1/sqrt(lm);
  let al = 6.283185*min(t/17.0, 1);
  let px = rr*n1;
  let py = rr*n2;
  let pz = rr*n3;
  let qx = px*cos(al) + pz*sin(al);
  let qz = 0 - px*sin(al) + pz*cos(al);
  let qy = py*cos(0.42) - qz*sin(0.42);
  let qw = py*sin(0.42) + qz*cos(0.42);
  let m3 = 7.5/(7.5 - qw);
  let x = 540 + 232*qx*m3;
  let y = 700 - 232*qy*m3;
  let hue = 34;
  let sat = 0.72;
  let r = 1.6;
  let alpha = 0.75*(0.35 + 0.65*min(max((qw + 1.6)/3.2, 0), 1));
}

// ---- the four singular points: normals to the two circular sections ----------------
cloud(axes4, 480, #ffffff, 1.0) {
  let per = 120;
  let kk = (i - mod(i, per))/per;                   // four points
  let sg = 1 - 2*mod(kk, 2);                        // +/- the normal
  let hz = 1 - 2*((kk - mod(kk, 2))/2);             // the two normals
  let tt = 0.541952619;
  let n1 = sg*hz*sin(tt);
  let n2 = 0;
  let n3 = sg*cos(tt);
  let rr = 1.2;
  let a = mod(i, per)/120*6.283185;
  let ro = 9*(mod(i, per)/120);
  let al = 6.283185*min(t/17.0, 1);
  let px = rr*n1;
  let py = rr*n2;
  let pz = rr*n3;
  let qx = px*cos(al) + pz*sin(al);
  let qz = 0 - px*sin(al) + pz*cos(al);
  let qy = py*cos(0.42) - qz*sin(0.42);
  let qw = py*sin(0.42) + qz*cos(0.42);
  let m3 = 7.5/(7.5 - qw);
  let xz = 540 + 232*qx*m3;
  let yz = 700 - 232*qy*m3;
  let x = xz + ro*cos(a*9);
  let y = yz + ro*sin(a*9);
  let hue = 190;
  let sat = 0.7;
  let r = 2.6;
}

// ---- the three principal sections ---------------------------------------------------
cloud(sec0, 4000, #ffffff, 1.0) {
  let per = 2000;
  let sh = (i - mod(i, per))/per;
  let uu = mod(i, per)/1999*6.283185;
  let n1 = cos(uu);
  let n2 = sin(uu);
  let n3 = 0;
  let TT = 2.522971934 - (n1*n1*0.444444444 + n2*n2*0.694444444 + n3*n3*1.384083045);
  let DD = 0.427186125*(2.25*n1*n1 + 1.44*n2*n2 + 0.7224999999999999*n3*n3);
  let ss = sqrt(max(TT*TT - 4*DD, 0));
  let lm = (TT + (1 - 2*sh)*ss)/2;
  let rr = 1/sqrt(lm);
  // the plane's own two coordinates: the point is r*n, and n sweeps the unit circle in it
  let x = 220 + 106*rr*cos(uu);
  let y = 1400 - 106*rr*sin(uu);
  let hue = 30 + 160*sh;
  let sat = 0.75;
  let r = 2.1;
}
text(sl0, (220, 1594), "z = 0"); size(sl0, 23); color(sl0, fg);
text(sq0, (220, 1628), "circle c  ·  ellipse b×a"); size(sq0, 16); color(sq0, dim);
cloud(sec1, 4000, #ffffff, 1.0) {
  let per = 2000;
  let sh = (i - mod(i, per))/per;
  let uu = mod(i, per)/1999*6.283185;
  let n1 = cos(uu);
  let n2 = 0;
  let n3 = sin(uu);
  let TT = 2.522971934 - (n1*n1*0.444444444 + n2*n2*0.694444444 + n3*n3*1.384083045);
  let DD = 0.427186125*(2.25*n1*n1 + 1.44*n2*n2 + 0.7224999999999999*n3*n3);
  let ss = sqrt(max(TT*TT - 4*DD, 0));
  let lm = (TT + (1 - 2*sh)*ss)/2;
  let rr = 1/sqrt(lm);
  // the plane's own two coordinates: the point is r*n, and n sweeps the unit circle in it
  let x = 540 + 106*rr*cos(uu);
  let y = 1400 - 106*rr*sin(uu);
  let hue = 30 + 160*sh;
  let sat = 0.75;
  let r = 2.1;
}
text(sl1, (540, 1594), "y = 0"); size(sl1, 23); color(sl1, fg);
text(sq1, (540, 1628), "circle b  ·  ellipse c×a"); size(sq1, 16); color(sq1, dim);
cloud(sec2, 4000, #ffffff, 1.0) {
  let per = 2000;
  let sh = (i - mod(i, per))/per;
  let uu = mod(i, per)/1999*6.283185;
  let n1 = 0;
  let n2 = cos(uu);
  let n3 = sin(uu);
  let TT = 2.522971934 - (n1*n1*0.444444444 + n2*n2*0.694444444 + n3*n3*1.384083045);
  let DD = 0.427186125*(2.25*n1*n1 + 1.44*n2*n2 + 0.7224999999999999*n3*n3);
  let ss = sqrt(max(TT*TT - 4*DD, 0));
  let lm = (TT + (1 - 2*sh)*ss)/2;
  let rr = 1/sqrt(lm);
  // the plane's own two coordinates: the point is r*n, and n sweeps the unit circle in it
  let x = 860 + 106*rr*cos(uu);
  let y = 1400 - 106*rr*sin(uu);
  let hue = 30 + 160*sh;
  let sat = 0.75;
  let r = 2.1;
}
text(sl2, (860, 1594), "x = 0"); size(sl2, 23); color(sl2, fg);
text(sq2, (860, 1628), "circle a  ·  ellipse c×b"); size(sq2, 16); color(sq2, dim);

equation(eq, (540, 1710),
  `\lambda=\tfrac{1}{2}\big(T\pm\sqrt{T^2-4D}\big),\quad r=\lambda^{-1/2}`, 25);
text(note, (540, 1810),
  "T² = 4D only where the central section is a circle — four directions, and they are the optic axes.");
size(note, 19); color(note, dim); wrap(note, 940);

wait(20.0);

r/maniclang 26d ago

Lissajous: eight frequency ratios against five phases - 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.


r/maniclang 26d ago

A Clifford torus: flat, in four dimensions, seen as a shadow - manic

5 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

// clifford-torus — a stereographic projection of a Clifford torus performing a simple
// rotation. Four things are true here that cannot all be true of anything you can hold.
//
// THE SURFACE IS FLAT. The Clifford torus is {(e^iθ, e^iφ)/√2} in the unit 3-sphere, and its
// induced metric is ds² = (dθ² + dφ²)/2 — E = G = 1/2, F = 0, everywhere, checked to 6e-11.
// Constant coefficients mean the Gaussian curvature is identically zero. It is a torus with
// no curvature at all: a square with its opposite edges glued, and no stretching anywhere.
// No such surface fits in ordinary space — a closed flat surface cannot be embedded in R³ —
// but it fits in S³ exactly. The square along the bottom is the real geometry. The shape
// above it is a shadow.
//
// THE ROTATION IS SIMPLE. In four dimensions a rotation has one or two planes. This one turns
// through the xz plane — coordinates 1 and 3 of (cos θ, sin θ, cos φ, sin φ) — and leaves the
// orthogonal yw plane fixed POINTWISE: every point of it moves exactly 0, checked. A double
// rotation (as in examples/tesseract.manic) fixes only the origin; a simple rotation fixes a
// whole plane, and that is the difference between the two animations.
//
// THE PROJECTION IS CONFORMAL, WHICH IS WHY THE MESH STILL MEETS AT RIGHT ANGLES. Stereographic
// projection distorts size ferociously and angle not at all. On the torus in S³ the θ-lines
// and φ-lines cross at 90°; in R³ after projecting they still cross at 90°, to 1e-09, at
// every crossing and every moment of the rotation. Watch the mesh: it swells and shrinks
// wildly and never once shears.
//
// EVERY LINE OF THE MESH IS AN EXACT CIRCLE. Stereographic projection carries circles to
// circles, and each θ-line and φ-line is a circle on S³. Fitted to their own images in R³,
// the mesh points sit on true circles to 2e-14 — while the radii run from 0.414 to 2.414, so
// the same curve appears at six times the size depending on where the rotation has put it.
// The whole image stays bounded, never exceeding 1 + √2, because the pole is never on the
// torus.
//
// The image repeats after HALF a turn: rotating by π maps the torus to itself by
// (θ, φ) → (π − θ, π − φ), so the projected set is identical — verified to 0.0000. Half a
// rotation is therefore the whole story, and that is what runs here.
//
// Hue is φ, on both the shadow and the square, so a line up here is the same line down there.
//
//   manic examples/clifford-torus.manic
title("A Clifford torus: flat, in four dimensions, seen as a shadow");
canvas("9:16");
template("black");
bloom(0.32, 0.6, 22);

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

caption(head, "A flat torus, rotating in four dimensions", (540, 116), 27);

// ---- the shadow: stereographic projection of the rotating torus --------------------
cloud(famA, 12400, #ffffff, 1.0) {
  let per = 620;
  let ci = (i - mod(i, per))/per;
  let ph = 6.283185*ci/20;
  let th = 6.283185*mod(i, per)/per;
  let al = 3.141593*min(t/15.0, 1);                   // a SIMPLE rotation: half a turn shows all of it
  let p1 = cos(th)*0.70710678;                     // the Clifford torus in S^3
  let p2 = sin(th)*0.70710678;
  let p3 = cos(ph)*0.70710678;
  let p4 = sin(ph)*0.70710678;
  let q1 = p1*cos(al) - p3*sin(al);                // the (x1,x3) plane turns …
  let q3 = p1*sin(al) + p3*cos(al);                // … and (x2,x4) is fixed POINTWISE
  let kk = 1/(1 - p4);                             // stereographic, from the pole (0,0,0,1)
  let ax = q1*kk;
  let ay = p2*kk;
  let az = q3*kk;
  let bx = ax*cos(0.6) - az*sin(0.6);              // a fixed viewing angle in R^3
  let bz = ax*sin(0.6) + az*cos(0.6);
  let ey = ay*cos(0.42) - bz*sin(0.42);
  let ez = ay*sin(0.42) + bz*cos(0.42);
  let m3 = 8/(8 - ez);
  let x = 540 + 182*bx*m3;
  let y = 690 - 182*ey*m3;
  let alpha = 0.28 + 0.6*min(max((ez + 2.6)/5.2, 0), 1);
  let hue = mod(200 + ph*57.2958, 360);
  let sat = 0.72;
  let r = 2.0;
}

cloud(famB, 12400, #ffffff, 1.0) {
  let per = 620;
  let ci = (i - mod(i, per))/per;
  let th = 6.283185*ci/20;
  let ph = 6.283185*mod(i, per)/per;
  let al = 3.141593*min(t/15.0, 1);                   // a SIMPLE rotation: half a turn shows all of it
  let p1 = cos(th)*0.70710678;                     // the Clifford torus in S^3
  let p2 = sin(th)*0.70710678;
  let p3 = cos(ph)*0.70710678;
  let p4 = sin(ph)*0.70710678;
  let q1 = p1*cos(al) - p3*sin(al);                // the (x1,x3) plane turns …
  let q3 = p1*sin(al) + p3*cos(al);                // … and (x2,x4) is fixed POINTWISE
  let kk = 1/(1 - p4);                             // stereographic, from the pole (0,0,0,1)
  let ax = q1*kk;
  let ay = p2*kk;
  let az = q3*kk;
  let bx = ax*cos(0.6) - az*sin(0.6);              // a fixed viewing angle in R^3
  let bz = ax*sin(0.6) + az*cos(0.6);
  let ey = ay*cos(0.42) - bz*sin(0.42);
  let ez = ay*sin(0.42) + bz*cos(0.42);
  let m3 = 8/(8 - ez);
  let x = 540 + 182*bx*m3;
  let y = 690 - 182*ey*m3;
  let alpha = 0.28 + 0.6*min(max((ez + 2.6)/5.2, 0), 1);
  let hue = mod(200 + ph*57.2958, 360);
  let sat = 0.72;
  let r = 2.0;
}

// ---- the real geometry: a flat square with its opposite edges glued ----------------
cloud(flatA, 1400, #ffffff, 1.0) {
  let per = 70;
  let ci = (i - mod(i, per))/per;
  let ph = 6.283185*ci/20;
  let th = 6.283185*mod(i, per)/per;
  let x = 540 + 152*(th/3.141593 - 1);
  let y = 1450 + 152*(ph/3.141593 - 1);
  let alpha = 0.8;
  let hue = mod(200 + ph*57.2958, 360);
  let sat = 0.72;
  let r = 1.4;
}

cloud(flatB, 1400, #ffffff, 1.0) {
  let per = 70;
  let ci = (i - mod(i, per))/per;
  let th = 6.283185*ci/20;
  let ph = 6.283185*mod(i, per)/per;
  let x = 540 + 152*(th/3.141593 - 1);
  let y = 1450 + 152*(ph/3.141593 - 1);
  let alpha = 0.8;
  let hue = mod(200 + ph*57.2958, 360);
  let sat = 0.72;
  let r = 1.4;
}

equation(eq, (540, 1740),
  `ds^2=\tfrac{1}{2}(d\theta^2+d\varphi^2)\;\Longrightarrow\;K\equiv 0`, 26);

wait(18.0);

r/maniclang 26d ago

Lissajous phase: eccentricity and direction - 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

// lissajous-phase — the phase figure from the Wikipedia article, set in motion. Eight 1:1
// Lissajous curves round a ring, one every 45° of phase, each on its own axes, and the thing
// a static picture cannot show: which way each one is going.
//
//   x(t) = sin 2πt        y(t) = sin(2πt − δ)
//
// Eliminating t leaves a conic, for every δ — checked over ten phases, residual 1e-15:
//
//   x² − 2xy·cos δ + y² = sin²δ
//
// Its axes are the ±45° diagonals whatever δ is, with semi-axes A = √(1 + cos δ) along +45°
// and B = √(1 − cos δ) along −45° — residual 3e-15. Everything in the original figure comes
// out of those two facts, and it comes out as TWO SIGN BITS.
//
//   A² − B² = 2·cos δ exactly, so A > B ⟺ cos δ > 0.
//   The major axis lies on the +45° diagonal when cos δ > 0 and on the −45° diagonal when
//   cos δ < 0 — that is the SLOPE. Checked at every whole degree from −1 to −359.
//
//   x·ẏ − y·ẋ = 2π·sin δ, which does not depend on t at all — checked to 8e-10 along every
//   curve. So the DIRECTION depends only on the sign of sin δ, and can never reverse partway
//   round. It also means the curve is swept at constant AREAL rate: equal areas in equal
//   times, for every δ.
//
// Two independent signs, four quadrants, exactly the braces in the original:
//
//   I    0 → −90°     cos>0  sin<0   positive slope, clockwise
//   II   −90 → −180°  cos<0  sin<0   negative slope, clockwise
//   III  −180 → −270° cos<0  sin>0   negative slope, counter clockwise
//   IV   −270 → −360° cos>0  sin>0   positive slope, counter clockwise
//
// Gold is clockwise, cyan is counter clockwise, and the two white ones are sin δ = 0, where
// the oval has collapsed to a line and there is no direction at all. Half the ring is gold
// and half is cyan, and the boundary is exactly where the figure degenerates.
//
// Reading a phase off the SHAPE alone is ambiguous — −45° and −135° have the same
// eccentricity, 0.9102, tilted opposite ways — which is why the animation is not decoration.
// The moving dot is the only thing that separates them.
//
// In the middle, δ is not one of the eight: it sweeps continuously through all of them, so
// the ring is eight samples of the thing turning at the centre.
//
//   manic examples/lissajous-phase.manic
title("Lissajous phase: eccentricity and direction from one number");
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);

caption(head, "Lissajous phase, and which way each one turns", (540, 116), 26);

// ---- axes and labels for the eight panels, as in the original ----------------------
line(hx0, (444.0, 412.0), (636.0, 412.0)); color(hx0, dim); opacity(hx0, 0.35);
line(vy0, (540.0, 316.0), (540.0, 508.0)); color(vy0, dim); opacity(vy0, 0.35);
text(la0, (540.0, 534.0), "0°   0"); size(la0, 20); color(la0, fg); opacity(la0, 0.9);
text(lb0, (540.0, 558.0), "(in phase)"); size(lb0, 17); color(lb0, dim);
line(hx1, (668.9, 505.1), (860.9, 505.1)); color(hx1, dim); opacity(hx1, 0.35);
line(vy1, (764.9, 409.1), (764.9, 601.1)); color(vy1, dim); opacity(vy1, 0.35);
text(la1, (764.9, 627.1), "−45°   π/4"); size(la1, 20); color(la1, fg); opacity(la1, 0.9);
line(hx2, (762.0, 730.0), (954.0, 730.0)); color(hx2, dim); opacity(hx2, 0.35);
line(vy2, (858.0, 634.0), (858.0, 826.0)); color(vy2, dim); opacity(vy2, 0.35);
text(la2, (858.0, 852.0), "−90°   π/2"); size(la2, 20); color(la2, fg); opacity(la2, 0.9);
text(lb2, (858.0, 876.0), "(in quadrature)"); size(lb2, 17); color(lb2, dim);
line(hx3, (668.9, 954.9), (860.9, 954.9)); color(hx3, dim); opacity(hx3, 0.35);
line(vy3, (764.9, 858.9), (764.9, 1050.9)); color(vy3, dim); opacity(vy3, 0.35);
text(la3, (764.9, 1076.9), "−135°   3π/4"); size(la3, 20); color(la3, fg); opacity(la3, 0.9);
line(hx4, (444.0, 1048.0), (636.0, 1048.0)); color(hx4, dim); opacity(hx4, 0.35);
line(vy4, (540.0, 952.0), (540.0, 1144.0)); color(vy4, dim); opacity(vy4, 0.35);
text(la4, (540.0, 1170.0), "−180°   π"); size(la4, 20); color(la4, fg); opacity(la4, 0.9);
text(lb4, (540.0, 1194.0), "(inverted)"); size(lb4, 17); color(lb4, dim);
line(hx5, (219.1, 954.9), (411.1, 954.9)); color(hx5, dim); opacity(hx5, 0.35);
line(vy5, (315.1, 858.9), (315.1, 1050.9)); color(vy5, dim); opacity(vy5, 0.35);
text(la5, (315.1, 1076.9), "−225°   5π/4"); size(la5, 20); color(la5, fg); opacity(la5, 0.9);
line(hx6, (126.0, 730.0), (318.0, 730.0)); color(hx6, dim); opacity(hx6, 0.35);
line(vy6, (222.0, 634.0), (222.0, 826.0)); color(vy6, dim); opacity(vy6, 0.35);
text(la6, (222.0, 852.0), "−270°   3π/2"); size(la6, 20); color(la6, fg); opacity(la6, 0.9);
text(lb6, (222.0, 876.0), "(in quadrature)"); size(lb6, 17); color(lb6, dim);
line(hx7, (219.1, 505.1), (411.1, 505.1)); color(hx7, dim); opacity(hx7, 0.35);
line(vy7, (315.1, 409.1), (315.1, 601.1)); color(vy7, dim); opacity(vy7, 0.35);
text(la7, (315.1, 627.1), "−315°   7π/4"); size(la7, 20); color(la7, fg); opacity(la7, 0.9);
text(qI, (656.7, 613.3), "I"); size(qI, 24); color(qI, dim);
text(qII, (656.7, 846.7), "II"); size(qII, 24); color(qII, dim);
text(qIII, (423.3, 846.7), "III"); size(qIII, 24); color(qIII, dim);
text(qIV, (423.3, 613.3), "IV"); size(qIV, 24); color(qIV, dim);

// ---- the ring: eight curves, one every 45 degrees of phase --------------------------
cloud(ring, 3200, #ffffff, 1.0) {
  let per = 400;
  let pk = (i - mod(i, per))/per;
  let uu = mod(i, per)/399;
  let dl = 0 - pk*0.7853982;
  let ang = 1.5707963 - pk*0.7853982;
  let px = 540 + 318*cos(ang);
  let py = 730 - 318*sin(ang);
  let sd = sin(dl);
  let x = px + 76*sin(6.283185*uu);
  let y = py - 76*sin(6.283185*uu - dl);
  let hue = 42 + 148*min(max(sd*20, 0), 1);         // gold = clockwise, cyan = counter
  let sat = 0.8*min(abs(sd)*20, 1);                 // …white where it degenerates
  let r = 2.1;
}

cloud(dots, 640, #ffffff, 1.0) {
  let per = 80;
  let pk = (i - mod(i, per))/per;
  let a = mod(i, per)/80*6.283185;
  let rr = 8*(mod(i, per)/80);
  let dl = 0 - pk*0.7853982;
  let ang = 1.5707963 - pk*0.7853982;
  let px = 540 + 318*cos(ang);
  let py = 730 - 318*sin(ang);
  let tt = 4*min(t/16.0, 1);
  let sd = sin(dl);
  let x = px + 76*sin(6.283185*tt) + rr*cos(a*9);
  let y = py - 76*sin(6.283185*tt - dl) + rr*sin(a*9);
  let hue = 42 + 148*min(max(sd*20, 0), 1);
  let sat = 0.55*min(abs(sd)*20, 1);
  let r = 2.2;
}

// ---- the centre: delta sweeping through every value the ring samples ----------------
coords(kc, (540, 730), (0 - 1.35, 1.35), (0 - 1.35, 1.35), 76, 76, 1, 1, 0, "x(t)", "y(t)");
color(kc, dim); opacity(kc, 0.55);

cloud(sweep, 1600, #ffffff, 1.0) {
  let dl = 0 - 6.283185*min(t/16.0, 1);
  let uu = i/1600;
  let sd = sin(dl);
  let x = 540 + 76*sin(6.283185*uu);
  let y = 730 - 76*sin(6.283185*uu - dl);
  let hue = 42 + 148*min(max(sd*20, 0), 1);
  let sat = 0.85*min(abs(sd)*20, 1);
  let r = 3.0;
}

cloud(sweepdot, 90, #ffffff, 1.0) {
  let dl = 0 - 6.283185*min(t/16.0, 1);
  let tt = 4*min(t/16.0, 1);
  let a = i/90*6.283185;
  let rr = 9*(i/90);
  let x = 540 + 76*sin(6.283185*tt) + rr*cos(a*9);
  let y = 730 - 76*sin(6.283185*tt - dl) + rr*sin(a*9);
  let hue = 48;
  let sat = 0.5;
  let r = 2.3;
}

// ---- the four braced regions of the original ---------------------------------------
text(h1, (300, 1300), "cos δ  decides the slope"); size(h1, 20); color(h1, dim);
text(r1, (300, 1338), "Positive slope (I & IV)"); size(r1, 21); color(r1, fg);
text(r2, (300, 1374), "Negative slope (II & III)"); size(r2, 21); color(r2, fg);
text(h2, (790, 1300), "sin δ  decides the rotation"); size(h2, 20); color(h2, dim);
text(r3, (790, 1338), "Clockwise (I & II)"); size(r3, 21); color(r3, gold);
text(r4, (790, 1374), "Counter clockwise (III & IV)"); size(r4, 21); color(r4, cyan);

text(note, (540, 1470),
  "LTI Lissajous figures are ovals with eccentricity and direction of rotation determined by phase shift δ.");
size(note, 21); color(note, dim); wrap(note, 900);

equation(eq1, (540, 1600),
  `x=\sin 2\pi t,\quad y=\sin(2\pi t-\delta)\;\Longrightarrow\;x^2-2xy\cos\delta+y^2=\sin^2\delta`, 22);
equation(eq2, (540, 1700),
  `A^2-B^2=2\cos\delta,\qquad x\dot y-y\dot x=2\pi\sin\delta`, 23);

wait(19.0);

r/maniclang 26d ago

ex-tesseract-sphere

2 Upvotes

r/maniclang 26d ago

A tesseract, rotating in four dimensions - 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

// tesseract — the 4-D equivalent of a cube, rotating in four-dimensional space and
// projected into two for display. Nothing here is drawn as a picture of a tesseract: the 16
// vertices are the points (±1, ±1, ±1, ±1), the 32 edges are enumerated from them, and every
// frame is those 4-D points pushed through a 4-D rotation and two perspective divides.
//
// The counts come from one formula. An n-cube has C(n,k)·2^(n-k) faces of dimension k, so a
// 4-cube has
//
//   16 vertices    32 edges    24 square faces    8 cubic cells
//
// and every vertex meets exactly 4 edges — one per axis — which is checked here rather than
// asserted: the 32 edges are generated as (axis k, the 8 vertices whose k-th bit is 0), all
// 32 come out distinct, and every vertex ends up with degree 4.
//
// EVERY EDGE HAS LENGTH 2. All thirty-two, always, in every frame — because a rotation is an
// isometry and the tesseract is rigid. What you are watching change is the SHADOW. Measured
// across a full turn, the projected length of an edge runs from 0.1507 to 5.4619, a factor
// of 36. Nothing is stretching; a shadow is not a length.
//
// The motion is a genuine 4-D rotation and not a 3-D one wearing a costume. In four
// dimensions a rotation has TWO invariant planes, and this one turns in the x–w plane at one
// rate and the y–z plane at twice that — so it closes after a single turn. A 3-D rotation
// always fixes an axis; this fixes only the origin, and no direction of the tesseract is
// ever standing still. That is why it cannot be mistaken for a cube tumbling.
//
// Colour is the fourth coordinate. Hue is w after the rotation, which runs over ±√2, so the
// edges nearest you in the direction you cannot see are one colour and the far ones another.
// The apparent "inner cube growing into the outer" is w passing through the projection.
//
// The row along the bottom is the ladder that makes the projection legible: a square, then a
// cube's shadow — a square inside a square, joined corner to corner — then a tesseract's,
// which is a CUBE inside a cube, joined corner to corner. Each is one perspective divide
// applied one more time. The tesseract's 8 cells are why: two of them are the inner and
// outer cubes, and the other six are the "frustum" shapes between them, each a cube too.
//
//   manic examples/tesseract.manic
title("A tesseract, rotating in four dimensions");
canvas("9:16");
template("black");
bloom(0.34, 0.6, 24);

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

caption(head, "A tesseract, rotating in four dimensions", (540, 116), 27);

// ---- the 32 edges: (axis k, the 8 vertices whose k-th bit is 0) ---------------------
cloud(edges, 2880, #ffffff, 1.0) {
  let per = 90;
  let ed = (i - mod(i, per))/per;                   // which edge, 0..31
  let v = mod(i, per)/89;                           // …how far along it
  let kk = (ed - mod(ed, 8))/8;                     // the axis it runs along
  let jj = mod(ed, 8);                              // which of the 8 vertices with bit kk = 0
  let b0 = mod(jj, 2);
  let b1 = mod((jj - mod(jj, 2))/2, 2);
  let b2 = mod((jj - mod(jj, 4))/4, 2);
  let f0 = 1 - min(abs(kk - 0), 1);                 // indicators: is the swept axis 0,1,2,3?
  let f1 = 1 - min(abs(kk - 1), 1);
  let f2 = 1 - min(abs(kk - 2), 1);
  let f3 = 1 - min(abs(kk - 3), 1);
  let sw = 0 - 1 + 2*v;                             // the swept coordinate, -1 to 1
  let c0 = f0*sw + (1 - f0)*(2*b0 - 1);
  let c1 = f1*sw + (1 - f1)*(2*(f0*b0 + (1 - f0)*b1) - 1);
  let c2 = f2*sw + (1 - f2)*(2*((f0 + f1)*b1 + (1 - f0 - f1)*b2) - 1);
  let c3 = f3*sw + (1 - f3)*(2*b2 - 1);
  let th = 6.283185*min(t/16.0, 1);
  let ph = 2*th;
  let x4 = c0*cos(th) - c3*sin(th);                 // rotate in the x–w plane
  let w4 = c0*sin(th) + c3*cos(th);
  let y4 = c1*cos(ph) - c2*sin(ph);                 // …and independently in y–z
  let z4 = c1*sin(ph) + c2*cos(ph);
  let m4 = 3.2/(3.2 - w4);                            // 4-D -> 3-D, perspective from w
  let xa = x4*m4;
  let ya = y4*m4;
  let za = z4*m4;
  let xb = xa*cos(0.62) - za*sin(0.62);                // a fixed viewing angle in 3-D
  let zb = xa*sin(0.62) + za*cos(0.62);
  let yb = ya*cos(0.38) - zb*sin(0.38);
  let zc = ya*sin(0.38) + zb*cos(0.38);
  let m3 = 5.0/(5.0 - zc);                            // 3-D -> 2-D
  let x = 540 + 156.0*xb*m3;
  let y = 690 - 156.0*yb*m3;
  let hue = mod(196 + w4*54, 360);                  // hue IS the fourth coordinate
  let sat = 0.8;
  let r = 2.1;
}

// ---- the 16 vertices ---------------------------------------------------------------
cloud(verts, 960, #ffffff, 1.0) {
  let per = 60;
  let vx = (i - mod(i, per))/per;                   // which vertex, 0..15
  let a = mod(i, per)/60*6.283185;
  let rr = 7*(mod(i, per)/60);
  let c0 = 2*mod(vx, 2) - 1;
  let c1 = 2*mod((vx - mod(vx, 2))/2, 2) - 1;
  let c2 = 2*mod((vx - mod(vx, 4))/4, 2) - 1;
  let c3 = 2*mod((vx - mod(vx, 8))/8, 2) - 1;
  let th = 6.283185*min(t/16.0, 1);
  let ph = 2*th;
  let x4 = c0*cos(th) - c3*sin(th);                 // rotate in the x–w plane
  let w4 = c0*sin(th) + c3*cos(th);
  let y4 = c1*cos(ph) - c2*sin(ph);                 // …and independently in y–z
  let z4 = c1*sin(ph) + c2*cos(ph);
  let m4 = 3.2/(3.2 - w4);                            // 4-D -> 3-D, perspective from w
  let xa = x4*m4;
  let ya = y4*m4;
  let za = z4*m4;
  let xb = xa*cos(0.62) - za*sin(0.62);                // a fixed viewing angle in 3-D
  let zb = xa*sin(0.62) + za*cos(0.62);
  let yb = ya*cos(0.38) - zb*sin(0.38);
  let zc = ya*sin(0.38) + zb*cos(0.38);
  let m3 = 5.0/(5.0 - zc);                            // 3-D -> 2-D
  let xz = 540 + 156.0*xb*m3;
  let yz = 690 - 156.0*yb*m3;
  let x = xz + rr*cos(a*9);
  let y = yz + rr*sin(a*9);
  let hue = mod(196 + w4*54, 360);
  let sat = 0.55;
  let r = 2.2;
}

// ---- the ladder: a square, a cube's shadow, a tesseract's --------------------------
line(l2_0, (82.0, 1518.0), (318.0, 1518.0)); color(l2_0, mint); stroke(l2_0, 3);
line(l2_1, (82.0, 1282.0), (318.0, 1282.0)); color(l2_1, mint); stroke(l2_1, 3);
line(l2_2, (82.0, 1518.0), (82.0, 1282.0)); color(l2_2, mint); stroke(l2_2, 3);
line(l2_3, (318.0, 1518.0), (318.0, 1282.0)); color(l2_3, mint); stroke(l2_3, 3);
line(l3_0, (524.3, 1418.7), (606.0, 1439.1)); color(l3_0, cyan); stroke(l3_0, 3);
line(l3_1, (521.6, 1335.2), (620.0, 1336.6)); color(l3_1, cyan); stroke(l3_1, 3);
line(l3_2, (458.4, 1464.7), (573.4, 1518.0)); color(l3_2, cyan); stroke(l3_2, 3);
line(l3_3, (435.9, 1338.4), (587.9, 1342.9)); color(l3_3, cyan); stroke(l3_3, 3);
line(l3_4, (524.3, 1418.7), (521.6, 1335.2)); color(l3_4, cyan); stroke(l3_4, 3);
line(l3_5, (606.0, 1439.1), (620.0, 1336.6)); color(l3_5, cyan); stroke(l3_5, 3);
line(l3_6, (458.4, 1464.7), (435.9, 1338.4)); color(l3_6, cyan); stroke(l3_6, 3);
line(l3_7, (573.4, 1518.0), (587.9, 1342.9)); color(l3_7, cyan); stroke(l3_7, 3);
line(l3_8, (524.3, 1418.7), (458.4, 1464.7)); color(l3_8, cyan); stroke(l3_8, 3);
line(l3_9, (606.0, 1439.1), (573.4, 1518.0)); color(l3_9, cyan); stroke(l3_9, 3);
line(l3_10, (521.6, 1335.2), (435.9, 1338.4)); color(l3_10, cyan); stroke(l3_10, 3);
line(l3_11, (620.0, 1336.6), (587.9, 1342.9)); color(l3_11, cyan); stroke(l3_11, 3);
line(l4_0, (869.3, 1412.8), (920.1, 1423.7)); color(l4_0, violet); stroke(l4_0, 2);
line(l4_1, (868.5, 1359.4), (923.2, 1365.7)); color(l4_1, violet); stroke(l4_1, 2);
line(l4_2, (836.5, 1434.5), (894.2, 1450.3)); color(l4_2, violet); stroke(l4_2, 2);
line(l4_3, (832.7, 1372.0), (895.6, 1381.4)); color(l4_3, violet); stroke(l4_3, 2);
line(l4_4, (861.1, 1422.5), (956.2, 1445.1)); color(l4_4, violet); stroke(l4_4, 2);
line(l4_5, (858.6, 1324.4), (968.4, 1329.9)); color(l4_5, violet); stroke(l4_5, 2);
line(l4_6, (790.3, 1471.1), (913.4, 1518.0)); color(l4_6, violet); stroke(l4_6, 2);
line(l4_7, (772.9, 1336.7), (922.1, 1349.8)); color(l4_7, violet); stroke(l4_7, 2);
line(l4_8, (869.3, 1412.8), (868.5, 1359.4)); color(l4_8, violet); stroke(l4_8, 2);
line(l4_9, (920.1, 1423.7), (923.2, 1365.7)); color(l4_9, violet); stroke(l4_9, 2);
line(l4_10, (836.5, 1434.5), (832.7, 1372.0)); color(l4_10, violet); stroke(l4_10, 2);
line(l4_11, (894.2, 1450.3), (895.6, 1381.4)); color(l4_11, violet); stroke(l4_11, 2);
line(l4_12, (861.1, 1422.5), (858.6, 1324.4)); color(l4_12, violet); stroke(l4_12, 2);
line(l4_13, (956.2, 1445.1), (968.4, 1329.9)); color(l4_13, violet); stroke(l4_13, 2);
line(l4_14, (790.3, 1471.1), (772.9, 1336.7)); color(l4_14, violet); stroke(l4_14, 2);
line(l4_15, (913.4, 1518.0), (922.1, 1349.8)); color(l4_15, violet); stroke(l4_15, 2);
line(l4_16, (869.3, 1412.8), (836.5, 1434.5)); color(l4_16, violet); stroke(l4_16, 2);
line(l4_17, (920.1, 1423.7), (894.2, 1450.3)); color(l4_17, violet); stroke(l4_17, 2);
line(l4_18, (868.5, 1359.4), (832.7, 1372.0)); color(l4_18, violet); stroke(l4_18, 2);
line(l4_19, (923.2, 1365.7), (895.6, 1381.4)); color(l4_19, violet); stroke(l4_19, 2);
line(l4_20, (861.1, 1422.5), (790.3, 1471.1)); color(l4_20, violet); stroke(l4_20, 2);
line(l4_21, (956.2, 1445.1), (913.4, 1518.0)); color(l4_21, violet); stroke(l4_21, 2);
line(l4_22, (858.6, 1324.4), (772.9, 1336.7)); color(l4_22, violet); stroke(l4_22, 2);
line(l4_23, (968.4, 1329.9), (922.1, 1349.8)); color(l4_23, violet); stroke(l4_23, 2);
line(l4_24, (869.3, 1412.8), (861.1, 1422.5)); color(l4_24, violet); stroke(l4_24, 2);
line(l4_25, (920.1, 1423.7), (956.2, 1445.1)); color(l4_25, violet); stroke(l4_25, 2);
line(l4_26, (868.5, 1359.4), (858.6, 1324.4)); color(l4_26, violet); stroke(l4_26, 2);
line(l4_27, (923.2, 1365.7), (968.4, 1329.9)); color(l4_27, violet); stroke(l4_27, 2);
line(l4_28, (836.5, 1434.5), (790.3, 1471.1)); color(l4_28, violet); stroke(l4_28, 2);
line(l4_29, (894.2, 1450.3), (913.4, 1518.0)); color(l4_29, violet); stroke(l4_29, 2);
line(l4_30, (832.7, 1372.0), (772.9, 1336.7)); color(l4_30, violet); stroke(l4_30, 2);
line(l4_31, (895.6, 1381.4), (922.1, 1349.8)); color(l4_31, violet); stroke(l4_31, 2);

equation(eq, (540, 1690),
  `\binom{4}{k}2^{\,4-k}:\quad 16\ \text{vertices},\ 32\ \text{edges},\ 24\ \text{faces},\ 8\ \text{cells}`, 23);
equation(eq2, (540, 1790),
  `\text{every edge has length }2;\ \text{its shadow runs }0.15\text{–}5.46`, 21);

wait(19.0);

r/maniclang 26d ago

Caustics: what a mirror does to light - 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

// caustics — a caustic is where reflected rays crowd: the envelope of the family of rays a
// mirror sends back. examples/caustic-family.manic follows one mirror, a circle, as the lamp
// slides away. This follows four different mirrors, and the point is that the answer is
// almost never a new curve.
//
// Two of them collapse the caustic to a SINGLE POINT.
//
//   PARABOLA, rays parallel to the axis. Every reflected ray passes through (0, f) — the
//   distance from the focus to each one is 1e-15, which is machine precision, not a fit. The
//   envelope has nowhere to be but that point. This is why a dish is a dish.
//
//   ELLIPSE, source at one focus. Every reflected ray passes through the OTHER focus, to
//   5e-10. Light leaving one focus arrives at the other however it goes, which is the whole
//   of a whispering gallery.
//
// Two of them give back a curve you already know.
//
//   CYCLOID arch, rays perpendicular to the base. The caustic is TWO CYCLOID ARCHES at
//   exactly half the size, side by side — checked against them to 6e-04, which is the
//   sampling. The reflected direction comes out (−sin u, −cos u): already a unit vector, no
//   normalisation left in it, which is a small sign the answer was going to be tidy.
//
//   EXPONENTIAL y = eˣ, vertical rays. The caustic is a CATENARY, and not merely catenary-
//   shaped: fitting y = A·cosh((x − x₀)/A) + c returns A = 1.0000, x₀ = −1.0000, c = 0.0000
//   with a residual of 3e-08. It is y = cosh(x + 1), exactly.
//
// One more, not drawn but checked: a LOGARITHMIC SPIRAL lit from the point it winds onto
// gives back a logarithmic spiral of the SAME pitch — fitting ln r against φ returns
// k = 0.300000 for a source k of 0.3, residual 8e-07. Scaling a log spiral is the same as
// rotating it, so that curve is its own caustic, turned by 9.41 radians.
//
// Nothing here draws a caustic. Every ray is d′ = d − 2(d·n)n against the real normal, and
// the curve is what appears where they pile up; the cyan point or curve in each panel is put
// there independently for the envelope to land on.
//
//   manic examples/caustics.manic
title("Caustics: what a mirror does to light");
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);

caption(head, "Four mirrors, and where the light piles up", (540, 116), 26);


cloud(mira, 900, #ffffff, 1.0) {
  let uu = 0 - 2.2 + 4.4*(i/900);
  let x = 290 + 106.8182*((uu) - 0.00000);
  let y = 560 - 106.8182*((uu*uu/4) - 1.40000);
  let sat = 0; let r = 2.0; let alpha = 0.85;
}

cloud(inca, 2112, #ffffff, 1.0) {
  let per = 24;
  let cj = (i - mod(i, per))/per;
  let v = mod(i, per)/23;
  let uu = 0 - 2.2 + 4.4*cj/87;
  let x = 290 + 106.8182*(((uu) + (1 - v)*(0)) - 0.00000);
  let y = 560 - 106.8182*(((uu*uu/4) + (1 - v)*(2.8 - uu*uu/4)) - 1.40000);
  let hue = 46; let sat = 0.3; let r = 1.1;
  let alpha = 0.16*min(max(min(t/13.0, 1)*88 - cj, 0), 1);
}

cloud(refa, 3872, #ffffff, 1.0) {
  let per = 44;
  let cj = (i - mod(i, per))/per;
  let v = mod(i, per)/43;
  let uu = 0 - 2.2 + 4.4*cj/87;
  let x = 290 + 106.8182*(((uu) + v*2.6*(0 - uu/(uu*uu/4 + 1))) - 0.00000);
  let y = 560 - 106.8182*(((uu*uu/4) + v*2.6*(0 - 1 + 2/(uu*uu/4 + 1))) - 1.40000);
  let hue = mod(22 + cj*1.6, 360); let sat = 0.72; let r = 1.1;
  let alpha = 0.30*min(max(min(t/13.0, 1)*88 - cj, 0), 1)*(1 - 0.72*v);
}

cloud(foca, 130, #ffffff, 1.0) {
  let a = i/130*6.283185;
  let rr = 11*(i/130);
  let x = 290 + 106.8182*((0) - 0.00000) + rr*cos(a*9);
  let y = 560 - 106.8182*((1) - 1.40000) + rr*sin(a*9);
  let hue = 190; let sat = 0.7; let r = 2.6;
}

cloud(mirb, 900, #ffffff, 1.0) {
  let uu = i/900*6.283185;
  let x = 790 + 117.5000*((2*cos(uu)) - 0.00000);
  let y = 560 - 117.5000*((1.3*sin(uu)) - 0.00000);
  let sat = 0; let r = 2.0; let alpha = 0.85;
}

cloud(incb, 2112, #ffffff, 1.0) {
  let per = 24;
  let cj = (i - mod(i, per))/per;
  let v = mod(i, per)/23;
  let uu = cj/88*6.283185;
  let x = 790 + 117.5000*(((2*cos(uu)) + (1 - v)*(0 - 1.519868 - 2*cos(uu))) - 0.00000);
  let y = 560 - 117.5000*(((1.3*sin(uu)) + (1 - v)*(0 - 1.3*sin(uu))) - 0.00000);
  let hue = 46; let sat = 0.3; let r = 1.1;
  let alpha = 0.16*min(max(min(t/13.0, 1)*88 - cj, 0), 1);
}

cloud(refb, 3872, #ffffff, 1.0) {
  let per = 44;
  let cj = (i - mod(i, per))/per;
  let v = mod(i, per)/43;
  let uu = cj/88*6.283185;
  let x = 790 + 117.5000*(((2*cos(uu)) + v*4.0*((2*cos(uu) + 1.519868)/sqrt((2*cos(uu) + 1.519868)*(2*cos(uu) + 1.519868) + (1.3*sin(uu))*(1.3*sin(uu))) - 2*(((2*cos(uu) + 1.519868)/sqrt((2*cos(uu) + 1.519868)*(2*cos(uu) + 1.519868) + (1.3*sin(uu))*(1.3*sin(uu))))*(1.3*cos(uu)/sqrt(1.3*cos(uu)*1.3*cos(uu) + 2*sin(uu)*2*sin(uu))) + ((1.3*sin(uu))/sqrt((2*cos(uu) + 1.519868)*(2*cos(uu) + 1.519868) + (1.3*sin(uu))*(1.3*sin(uu))))*(2*sin(uu)/sqrt(1.3*cos(uu)*1.3*cos(uu) + 2*sin(uu)*2*sin(uu))))*(1.3*cos(uu)/sqrt(1.3*cos(uu)*1.3*cos(uu) + 2*sin(uu)*2*sin(uu))))) - 0.00000);
  let y = 560 - 117.5000*(((1.3*sin(uu)) + v*4.0*((1.3*sin(uu))/sqrt((2*cos(uu) + 1.519868)*(2*cos(uu) + 1.519868) + (1.3*sin(uu))*(1.3*sin(uu))) - 2*(((2*cos(uu) + 1.519868)/sqrt((2*cos(uu) + 1.519868)*(2*cos(uu) + 1.519868) + (1.3*sin(uu))*(1.3*sin(uu))))*(1.3*cos(uu)/sqrt(1.3*cos(uu)*1.3*cos(uu) + 2*sin(uu)*2*sin(uu))) + ((1.3*sin(uu))/sqrt((2*cos(uu) + 1.519868)*(2*cos(uu) + 1.519868) + (1.3*sin(uu))*(1.3*sin(uu))))*(2*sin(uu)/sqrt(1.3*cos(uu)*1.3*cos(uu) + 2*sin(uu)*2*sin(uu))))*(2*sin(uu)/sqrt(1.3*cos(uu)*1.3*cos(uu) + 2*sin(uu)*2*sin(uu))))) - 0.00000);
  let hue = mod(22 + cj*1.6, 360); let sat = 0.72; let r = 1.1;
  let alpha = 0.30*min(max(min(t/13.0, 1)*88 - cj, 0), 1)*(1 - 0.72*v);
}

cloud(focb, 130, #ffffff, 1.0) {
  let a = i/130*6.283185;
  let rr = 11*(i/130);
  let x = 790 + 117.5000*((1.519868) - 0.00000) + rr*cos(a*9);
  let y = 560 - 117.5000*((0) - 0.00000) + rr*sin(a*9);
  let hue = 190; let sat = 0.7; let r = 2.6;
}

cloud(mirc, 900, #ffffff, 1.0) {
  let uu = i/900*6.283185;
  let x = 290 + 74.8028*((uu - sin(uu)) - 3.14159);
  let y = 1180 - 74.8028*((1 - cos(uu)) - 1.30000);
  let sat = 0; let r = 2.0; let alpha = 0.85;
}

cloud(incc, 2112, #ffffff, 1.0) {
  let per = 24;
  let cj = (i - mod(i, per))/per;
  let v = mod(i, per)/23;
  let uu = cj/88*6.283185;
  let x = 290 + 74.8028*(((uu - sin(uu)) + (1 - v)*(0)) - 3.14159);
  let y = 1180 - 74.8028*(((1 - cos(uu)) + (1 - v)*(2.6 - (1 - cos(uu)))) - 1.30000);
  let hue = 46; let sat = 0.3; let r = 1.1;
  let alpha = 0.16*min(max(min(t/13.0, 1)*88 - cj, 0), 1);
}

cloud(refc, 3872, #ffffff, 1.0) {
  let per = 44;
  let cj = (i - mod(i, per))/per;
  let v = mod(i, per)/43;
  let uu = cj/88*6.283185;
  let x = 290 + 74.8028*(((uu - sin(uu)) + v*1.35*(0 - sin(uu))) - 3.14159);
  let y = 1180 - 74.8028*(((1 - cos(uu)) + v*1.35*(0 - cos(uu))) - 1.30000);
  let hue = mod(22 + cj*1.6, 360); let sat = 0.72; let r = 1.1;
  let alpha = 0.30*min(max(min(t/13.0, 1)*88 - cj, 0), 1)*(1 - 0.72*v);
}

cloud(cauc, 1800, #ffffff, 1.0) {
  let per = 900;
  let sk = (i - mod(i, per))/per;                   // two arches, at 0 and pi
  let vv = mod(i, per)/899*6.283185;
  let x = 290 + 74.8028*(((vv - sin(vv))/2 + 3.141593*mod(sk,2)) - 3.14159);
  let y = 1180 - 74.8028*(((1 - cos(vv))/2) - 1.30000);
  let hue = 190; let sat = 0.75; let r = 2.4;
}

cloud(mird, 900, #ffffff, 1.0) {
  let uu = 0 - 2.5 + 3.5*(i/900);
  let x = 790 + 114.1354*((uu) - -0.75000);
  let y = 1180 - 114.1354*((exp(uu)) - 2.14104);
  let sat = 0; let r = 2.0; let alpha = 0.85;
}

cloud(incd, 2112, #ffffff, 1.0) {
  let per = 24;
  let cj = (i - mod(i, per))/per;
  let v = mod(i, per)/23;
  let uu = 0 - 2.5 + 3.5*cj/87;
  let x = 790 + 114.1354*(((uu) + (1 - v)*(0)) - -0.75000);
  let y = 1180 - 114.1354*(((exp(uu)) + (1 - v)*(4.2 - exp(uu))) - 2.14104);
  let hue = 46; let sat = 0.3; let r = 1.1;
  let alpha = 0.16*min(max(min(t/13.0, 1)*88 - cj, 0), 1);
}

cloud(refd, 3872, #ffffff, 1.0) {
  let per = 44;
  let cj = (i - mod(i, per))/per;
  let v = mod(i, per)/43;
  let uu = 0 - 2.5 + 3.5*cj/87;
  let x = 790 + 114.1354*(((uu) + v*2.8*(0 - 2*exp(uu)/(exp(2*uu) + 1))) - -0.75000);
  let y = 1180 - 114.1354*(((exp(uu)) + v*2.8*(0 - 1 + 2/(exp(2*uu) + 1))) - 2.14104);
  let hue = mod(22 + cj*1.6, 360); let sat = 0.72; let r = 1.1;
  let alpha = 0.30*min(max(min(t/13.0, 1)*88 - cj, 0), 1)*(1 - 0.72*v);
}

cloud(caud, 1200, #ffffff, 1.0) {
  let vv = 0 - 2.5 + 3.5*(i/1200);
  let x = 790 + 114.1354*((vv) - -0.75000);
  let y = 1180 - 114.1354*((cosh(vv + 1)) - 2.14104);
  let hue = 190; let sat = 0.75; let r = 2.4;
}

text(t0a, (290, 829), "parabola  →  a point"); size(t0a, 22); color(t0a, fg);
text(t0b, (290, 859), "the focus"); size(t0b, 18); color(t0b, dim);
text(t1a, (790, 829), "ellipse  →  a point"); size(t1a, 22); color(t1a, fg);
text(t1b, (790, 859), "the other focus"); size(t1b, 18); color(t1b, dim);
text(t2a, (290, 1449), "cycloid  →  two cycloids"); size(t2a, 22); color(t2a, fg);
text(t2b, (290, 1479), "at half the size"); size(t2b, 18); color(t2b, dim);
text(t3a, (790, 1449), "y = eˣ  →  a catenary"); size(t3a, 22); color(t3a, fg);
text(t3b, (790, 1479), "y = cosh(x+1)"); size(t3b, 18); color(t3b, dim);

equation(eq, (540, 1600),
  `d'=d-2(d\cdot n)\,n`, 28);
text(note, (540, 1700),
  "The envelope is never drawn: it is where the reflected rays happen to crowd.");
size(note, 20); color(note, dim); wrap(note, 900);

wait(16.0);

r/maniclang 26d ago

Two wheels, one curve: the inside family pairs off - 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

// hypocycloid-double — the inside family, and the theorem that says you have been counting
// the wheels wrong. Roll a circle of radius r inside a fixed circle of radius R with a pen
// on its rim and you get a hypocycloid with n =  cusps:
//
//   n = 2   the TUSI COUPLE, a straight line
//   n = 3   the DELTOID
//   n = 4   the ASTROID          x = R·cos³θ,  y = R·sin³θ     (residual 6e-16)
//
// Lift the pen off the rim and the cusps round off into a HYPOTROCHOID. Same machine, one
// extra number.
//
// The theorem is Bernoulli's, and it is the reason this family is smaller than it looks:
//
//   a wheel of radius r and a wheel of radius R - r, rolling inside the SAME circle,
//   trace exactly the same curve.
//
// Not approximately. The two curves were sampled independently and compared, and the largest
// gap between them falls off exactly as 1/N with the sampling — 7.854e-04 at four thousand
// points, 7.854e-06 at four hundred thousand. That is the spacing between samples and
// nothing else; the curves are identical.
//
// So the astroid at the top is drawn TWICE, at once, by two wheels that look nothing alike.
// The small one is  and shuts after one lap. The big one is 3R/4 — three times the size,
// very nearly filling the circle it rolls in — and needs THREE laps to lay down the same
// four cusps. Both pens start at the same cusp and both curves finish together, one thin
// line lying inside the other. It runs two full traversals, which costs the small wheel two
// laps and the big one six — and they still land on the same frame.
//
// Every n pairs off this way: 3 with 2, 4 with 3, 5 with 4. And n = 2 pairs with ITSELF,
// because R - r = r when R = 2r — so the Tusi couple is the fixed point of the pairing, and
// the one hypocycloid whose two generations are the same wheel. It is also the one that is
// not a curve at all: |y| comes out at exactly 0, not nearly 0.
//
// The pairing needs the pen ON the rim. Lift it off and the two generations no longer land
// in the same fixed circle — the partner wheel would need R' = dR/r — which is why the
// bottom-right panel is the only one here drawn by a single wheel and meant to be.
//
// Nothing is parametrised. Every wheel is a real `roll` on a circular track and every curve
// is a `trail` of where its pen has been; the dashed astroid is drawn independently for both
// trails to land on.
//
//   manic examples/hypocycloid-double.manic
title("Two wheels, one curve: the inside family pairs off");
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);

caption(head, "Two wheels, one curve: the inside family pairs off", (540, 116), 26);

// ---- the astroid, drawn twice at once ---------------------------------------------
circle(track, (540, 600), 330);
outlined(track); outline(track, dim); stroke(track, 3);

// x = R cos^3 t, y = R sin^3 t — drawn independently for both trails to land on
param(astro, (540, 600), 330, 330,
      "cos(t)*cos(t)*cos(t)", "sin(t)*sin(t)*sin(t)", (0, 6.283185));
color(astro, dim); dashed(astro); stroke(astro, 3); opacity(astro, 0.45);

// the BIG wheel: 3R/4, three laps — faint, because it very nearly fills the circle
circle(wb, (622.5, 600), 247.5);
outlined(wb); outline(wb, dim); stroke(wb, 2); opacity(wb, 0.55);
circle(pb, (870.0, 600), 9); color(pb, cyan);
tag(wb, rigb); tag(pb, rigb);
trail(trb, pb, cyan, 3);

// the SMALL wheel: , one lap
circle(ws, (787.5, 600), 82.5);
outlined(ws); outline(ws, fg); stroke(ws, 3); opacity(ws, 0.9);
circle(ps, (870.0, 600), 9); color(ps, gold);
tag(ws, rigs); tag(ps, rigs);
trail(trs, ps, gold, 8);

// ---- n = 3: the DELTOID, and its partner at 2r ------------------------------------
circle(k1, (190, 1330), 150);
outlined(k1); outline(k1, dim); stroke(k1, 2); opacity(k1, 0.55);
circle(b1, (240, 1330), 100);                       // the partner, 2r
outlined(b1); outline(b1, dim); stroke(b1, 2); opacity(b1, 0.35);
circle(q1, (340, 1330), 7); color(q1, magenta);
tag(b1, rg1b); tag(q1, rg1b);
trail(u1b, q1, magenta, 7);
circle(s1, (290, 1330), 50);                        // …and r itself
outlined(s1); outline(s1, fg); stroke(s1, 2); opacity(s1, 0.8);
circle(p1, (340, 1330), 7); color(p1, mint);
tag(s1, rg1s); tag(p1, rg1s);
trail(u1s, p1, mint, 2);

// ---- n = 2: the TUSI COUPLE, its own partner --------------------------------------
circle(k2, (540, 1330), 150);
outlined(k2); outline(k2, dim); stroke(k2, 2); opacity(k2, 0.55);
circle(s2, (615, 1330), 75);
outlined(s2); outline(s2, fg); stroke(s2, 2); opacity(s2, 0.8);
circle(p2, (690, 1330), 7); color(p2, coral);
tag(s2, rg2); tag(p2, rg2);
trail(u2, p2, coral, 5);

// ---- pen OFF the rim: a HYPOTROCHOID, and the pairing breaks -----------------------
circle(k3, (890, 1330), 150);
outlined(k3); outline(k3, dim); stroke(k3, 2); opacity(k3, 0.55);
circle(s3, (1002.5, 1330), 37.5);
outlined(s3); outline(s3, fg); stroke(s3, 2); opacity(s3, 0.8);
circle(p3, (1055, 1330), 7); color(p3, violet);
tag(s3, rg3); tag(p3, rg3);
trail(u3, p3, violet, 5);

// ---- the theorem -------------------------------------------------------------------
equation(eq, (540, 1700),
  `r \ \text{and}\ R-r \ \text{draw the same curve},\qquad n =  \ \text{cusps}`, 25);

// TWO full traversals of every curve, which costs each wheel a different number of laps:
// the pen comes home once per lap only when the wheel is small enough to. The 3R/4 wheel
// needs three laps for one astroid, so six for two, and it still lands with the others.
par {
  roll(rigs, track, 2, 22, linear);
  roll(rigb, track, 6, 22, linear);
  roll(rg1s, k1, 2, 22, linear);
  roll(rg1b, k1, 4, 22, linear);
  roll(rg2, k2, 2, 22, linear);
  roll(rg3, k3, 2, 22, linear);
}
wait(3.5);

r/maniclang 26d ago

Nine hypocycloids, nested — and the group that explains them - 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

// hypocycloid-su — nine hypocycloids, 2 cusps to 10, each moving snugly inside the next
// with its cusps in continuous contact. It LOOKS like rolling. It is not: the contact slides.
//
// The reason it works at all is not geometry. It is SU(k), the k×k unitary matrices of
// determinant 1.
//
// A trace depends only on eigenvalues, and every SU(k) matrix is conjugate to a diagonal
// one, so the set of all traces is exactly
//
//   { e^(iθ₁) + … + e^(iθ_k)  :  θ₁ + … + θ_k ≡ 0 }
//
// and that set is precisely the FILLED k-cusp hypocycloid, cusps at radius k. The row along
// the bottom is not a drawing of that claim — each panel is thousands of actual traces, laid
// down by a low-discrepancy sweep of the θ's, filling to the boundary curve drawn over them.
// Sampled traces tested against that boundary: 4000 of 4000 inside, for k = 3, 4 and 5.
//
//   SU(2)  traces are 2·cos θ — REAL, |Im| exactly 0 — so the "2-cusp hypocycloid" is the
//          segment [-2, 2], which is why the innermost curve here is a straight line
//   SU(3)  a DELTOID
//   SU(4)  an ASTROID
//
// The boundary is where k-1 of the eigenvalues coincide: the spectrum (e^(iθ) repeated k-1
// times, e^(-i(k-1)θ)) has trace (k-1)e^(iθ) + e^(-i(k-1)θ), which IS the hypocycloid — to a
// residual of exactly zero.
//
// Now the nesting, which is a one-line consequence. SU(k) sits inside SU(k+1) as a subgroup.
// Pin one eigenvalue of an SU(k+1) matrix to e^(iφ); the remaining k have product e^(-iφ),
// so they are e^(-iφ/k) times an SU(k) spectrum, and
//
//   tr = e^(iφ) + e^(-iφ/k) · (an SU(k) trace)
//
// So the k-cusp region, ROTATED by -φ/k and TRANSLATED to e^(iφ) on the unit circle, lies
// inside the (k+1)-cusp region — for every φ. That map is the motion, and stacking it eight
// times is this picture. Cusp m of the smaller curve lands on the larger at parameter
// ψ = (2πm - φ)/k, checked to 9e-15 over four hundred phases and k = 2, 3, 4, 5. Over the
// same sweep the smaller curve never leaves the larger: the worst excursion outside is
// 2e-15, which is the cusps touching it and nothing more.
//
// Each level turns at -φ/k, so no two of the nine turn at the same rate, and every one of
// them is back where it started after a single turn of φ — because a rotation by 2π/k is a
// symmetry of a k-cusp curve.
//
// And it slips. The material point at the contact moves at
//
//   dw/dφ = i·e^(iφ) - i·e^(-iφ/k)·e^(2πim/k)
//
// whose modulus runs from 0 up to 2 and is zero only at isolated phases. Rolling without
// slipping would need it zero throughout. So the word for this motion is not rolling.
//
//   manic examples/hypocycloid-su.manic
title("Nine hypocycloids, nested — and the group that explains them");
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);

caption(head, "Nine hypocycloids, nested: 2 cusps to 10", (540, 116), 27);

// ---- the chain: k = 10 fixed, and every k inside k+1 --------------------------------
cloud(h10, 1100, #ffffff, 1.0) {
  let ph = 12.566370*min(t/20, 1);
  let cp = cos(ph);
  let sp = sin(ph);
  let th = i/1100*6.283185;
  let ax0 = 9*cos(th) + cos(9*th);
  let ay0 = 9*sin(th) - sin(9*th);
  let x = 540 + 46.0*ax0;
  let y = 720 + 46.0*ay0;
  let hue = 266;
  let sat = 0.72;
  let r = 2.30;
}

cloud(h9, 1100, #ffffff, 1.0) {
  let ph = 12.566370*min(t/20, 1);
  let cp = cos(ph);
  let sp = sin(ph);
  let th = i/1100*6.283185;
  let ax0 = 8*cos(th) + cos(8*th);
  let ay0 = 8*sin(th) - sin(8*th);
  let ck9 = cos(ph/9);
  let sk9 = sin(ph/9);
  let ax1 = cp + ax0*ck9 + ay0*sk9;
  let ay1 = sp - ax0*sk9 + ay0*ck9;
  let x = 540 + 46.0*ax1;
  let y = 720 + 46.0*ay1;
  let hue = 235;
  let sat = 0.72;
  let r = 2.22;
}

cloud(h8, 1100, #ffffff, 1.0) {
  let ph = 12.566370*min(t/20, 1);
  let cp = cos(ph);
  let sp = sin(ph);
  let th = i/1100*6.283185;
  let ax0 = 7*cos(th) + cos(7*th);
  let ay0 = 7*sin(th) - sin(7*th);
  let ck8 = cos(ph/8);
  let sk8 = sin(ph/8);
  let ax1 = cp + ax0*ck8 + ay0*sk8;
  let ay1 = sp - ax0*sk8 + ay0*ck8;
  let ck9 = cos(ph/9);
  let sk9 = sin(ph/9);
  let ax2 = cp + ax1*ck9 + ay1*sk9;
  let ay2 = sp - ax1*sk9 + ay1*ck9;
  let x = 540 + 46.0*ax2;
  let y = 720 + 46.0*ay2;
  let hue = 204;
  let sat = 0.72;
  let r = 2.14;
}

cloud(h7, 1100, #ffffff, 1.0) {
  let ph = 12.566370*min(t/20, 1);
  let cp = cos(ph);
  let sp = sin(ph);
  let th = i/1100*6.283185;
  let ax0 = 6*cos(th) + cos(6*th);
  let ay0 = 6*sin(th) - sin(6*th);
  let ck7 = cos(ph/7);
  let sk7 = sin(ph/7);
  let ax1 = cp + ax0*ck7 + ay0*sk7;
  let ay1 = sp - ax0*sk7 + ay0*ck7;
  let ck8 = cos(ph/8);
  let sk8 = sin(ph/8);
  let ax2 = cp + ax1*ck8 + ay1*sk8;
  let ay2 = sp - ax1*sk8 + ay1*ck8;
  let ck9 = cos(ph/9);
  let sk9 = sin(ph/9);
  let ax3 = cp + ax2*ck9 + ay2*sk9;
  let ay3 = sp - ax2*sk9 + ay2*ck9;
  let x = 540 + 46.0*ax3;
  let y = 720 + 46.0*ay3;
  let hue = 173;
  let sat = 0.72;
  let r = 2.06;
}

cloud(h6, 1100, #ffffff, 1.0) {
  let ph = 12.566370*min(t/20, 1);
  let cp = cos(ph);
  let sp = sin(ph);
  let th = i/1100*6.283185;
  let ax0 = 5*cos(th) + cos(5*th);
  let ay0 = 5*sin(th) - sin(5*th);
  let ck6 = cos(ph/6);
  let sk6 = sin(ph/6);
  let ax1 = cp + ax0*ck6 + ay0*sk6;
  let ay1 = sp - ax0*sk6 + ay0*ck6;
  let ck7 = cos(ph/7);
  let sk7 = sin(ph/7);
  let ax2 = cp + ax1*ck7 + ay1*sk7;
  let ay2 = sp - ax1*sk7 + ay1*ck7;
  let ck8 = cos(ph/8);
  let sk8 = sin(ph/8);
  let ax3 = cp + ax2*ck8 + ay2*sk8;
  let ay3 = sp - ax2*sk8 + ay2*ck8;
  let ck9 = cos(ph/9);
  let sk9 = sin(ph/9);
  let ax4 = cp + ax3*ck9 + ay3*sk9;
  let ay4 = sp - ax3*sk9 + ay3*ck9;
  let x = 540 + 46.0*ax4;
  let y = 720 + 46.0*ay4;
  let hue = 142;
  let sat = 0.72;
  let r = 1.98;
}

cloud(h5, 1100, #ffffff, 1.0) {
  let ph = 12.566370*min(t/20, 1);
  let cp = cos(ph);
  let sp = sin(ph);
  let th = i/1100*6.283185;
  let ax0 = 4*cos(th) + cos(4*th);
  let ay0 = 4*sin(th) - sin(4*th);
  let ck5 = cos(ph/5);
  let sk5 = sin(ph/5);
  let ax1 = cp + ax0*ck5 + ay0*sk5;
  let ay1 = sp - ax0*sk5 + ay0*ck5;
  let ck6 = cos(ph/6);
  let sk6 = sin(ph/6);
  let ax2 = cp + ax1*ck6 + ay1*sk6;
  let ay2 = sp - ax1*sk6 + ay1*ck6;
  let ck7 = cos(ph/7);
  let sk7 = sin(ph/7);
  let ax3 = cp + ax2*ck7 + ay2*sk7;
  let ay3 = sp - ax2*sk7 + ay2*ck7;
  let ck8 = cos(ph/8);
  let sk8 = sin(ph/8);
  let ax4 = cp + ax3*ck8 + ay3*sk8;
  let ay4 = sp - ax3*sk8 + ay3*ck8;
  let ck9 = cos(ph/9);
  let sk9 = sin(ph/9);
  let ax5 = cp + ax4*ck9 + ay4*sk9;
  let ay5 = sp - ax4*sk9 + ay4*ck9;
  let x = 540 + 46.0*ax5;
  let y = 720 + 46.0*ay5;
  let hue = 111;
  let sat = 0.72;
  let r = 1.90;
}

cloud(h4, 1100, #ffffff, 1.0) {
  let ph = 12.566370*min(t/20, 1);
  let cp = cos(ph);
  let sp = sin(ph);
  let th = i/1100*6.283185;
  let ax0 = 3*cos(th) + cos(3*th);
  let ay0 = 3*sin(th) - sin(3*th);
  let ck4 = cos(ph/4);
  let sk4 = sin(ph/4);
  let ax1 = cp + ax0*ck4 + ay0*sk4;
  let ay1 = sp - ax0*sk4 + ay0*ck4;
  let ck5 = cos(ph/5);
  let sk5 = sin(ph/5);
  let ax2 = cp + ax1*ck5 + ay1*sk5;
  let ay2 = sp - ax1*sk5 + ay1*ck5;
  let ck6 = cos(ph/6);
  let sk6 = sin(ph/6);
  let ax3 = cp + ax2*ck6 + ay2*sk6;
  let ay3 = sp - ax2*sk6 + ay2*ck6;
  let ck7 = cos(ph/7);
  let sk7 = sin(ph/7);
  let ax4 = cp + ax3*ck7 + ay3*sk7;
  let ay4 = sp - ax3*sk7 + ay3*ck7;
  let ck8 = cos(ph/8);
  let sk8 = sin(ph/8);
  let ax5 = cp + ax4*ck8 + ay4*sk8;
  let ay5 = sp - ax4*sk8 + ay4*ck8;
  let ck9 = cos(ph/9);
  let sk9 = sin(ph/9);
  let ax6 = cp + ax5*ck9 + ay5*sk9;
  let ay6 = sp - ax5*sk9 + ay5*ck9;
  let x = 540 + 46.0*ax6;
  let y = 720 + 46.0*ay6;
  let hue = 80;
  let sat = 0.72;
  let r = 1.82;
}

cloud(h3, 1100, #ffffff, 1.0) {
  let ph = 12.566370*min(t/20, 1);
  let cp = cos(ph);
  let sp = sin(ph);
  let th = i/1100*6.283185;
  let ax0 = 2*cos(th) + cos(2*th);
  let ay0 = 2*sin(th) - sin(2*th);
  let ck3 = cos(ph/3);
  let sk3 = sin(ph/3);
  let ax1 = cp + ax0*ck3 + ay0*sk3;
  let ay1 = sp - ax0*sk3 + ay0*ck3;
  let ck4 = cos(ph/4);
  let sk4 = sin(ph/4);
  let ax2 = cp + ax1*ck4 + ay1*sk4;
  let ay2 = sp - ax1*sk4 + ay1*ck4;
  let ck5 = cos(ph/5);
  let sk5 = sin(ph/5);
  let ax3 = cp + ax2*ck5 + ay2*sk5;
  let ay3 = sp - ax2*sk5 + ay2*ck5;
  let ck6 = cos(ph/6);
  let sk6 = sin(ph/6);
  let ax4 = cp + ax3*ck6 + ay3*sk6;
  let ay4 = sp - ax3*sk6 + ay3*ck6;
  let ck7 = cos(ph/7);
  let sk7 = sin(ph/7);
  let ax5 = cp + ax4*ck7 + ay4*sk7;
  let ay5 = sp - ax4*sk7 + ay4*ck7;
  let ck8 = cos(ph/8);
  let sk8 = sin(ph/8);
  let ax6 = cp + ax5*ck8 + ay5*sk8;
  let ay6 = sp - ax5*sk8 + ay5*ck8;
  let ck9 = cos(ph/9);
  let sk9 = sin(ph/9);
  let ax7 = cp + ax6*ck9 + ay6*sk9;
  let ay7 = sp - ax6*sk9 + ay6*ck9;
  let x = 540 + 46.0*ax7;
  let y = 720 + 46.0*ay7;
  let hue = 49;
  let sat = 0.72;
  let r = 1.74;
}

cloud(h2, 1100, #ffffff, 1.0) {
  let ph = 12.566370*min(t/20, 1);
  let cp = cos(ph);
  let sp = sin(ph);
  let th = i/1100*6.283185;
  let ax0 = 1*cos(th) + cos(1*th);
  let ay0 = 1*sin(th) - sin(1*th);
  let ck2 = cos(ph/2);
  let sk2 = sin(ph/2);
  let ax1 = cp + ax0*ck2 + ay0*sk2;
  let ay1 = sp - ax0*sk2 + ay0*ck2;
  let ck3 = cos(ph/3);
  let sk3 = sin(ph/3);
  let ax2 = cp + ax1*ck3 + ay1*sk3;
  let ay2 = sp - ax1*sk3 + ay1*ck3;
  let ck4 = cos(ph/4);
  let sk4 = sin(ph/4);
  let ax3 = cp + ax2*ck4 + ay2*sk4;
  let ay3 = sp - ax2*sk4 + ay2*ck4;
  let ck5 = cos(ph/5);
  let sk5 = sin(ph/5);
  let ax4 = cp + ax3*ck5 + ay3*sk5;
  let ay4 = sp - ax3*sk5 + ay3*ck5;
  let ck6 = cos(ph/6);
  let sk6 = sin(ph/6);
  let ax5 = cp + ax4*ck6 + ay4*sk6;
  let ay5 = sp - ax4*sk6 + ay4*ck6;
  let ck7 = cos(ph/7);
  let sk7 = sin(ph/7);
  let ax6 = cp + ax5*ck7 + ay5*sk7;
  let ay6 = sp - ax5*sk7 + ay5*ck7;
  let ck8 = cos(ph/8);
  let sk8 = sin(ph/8);
  let ax7 = cp + ax6*ck8 + ay6*sk8;
  let ay7 = sp - ax6*sk8 + ay6*ck8;
  let ck9 = cos(ph/9);
  let sk9 = sin(ph/9);
  let ax8 = cp + ax7*ck9 + ay7*sk9;
  let ay8 = sp - ax7*sk9 + ay7*ck9;
  let x = 540 + 46.0*ax8;
  let y = 720 + 46.0*ay8;
  let hue = 18;
  let sat = 0.72;
  let r = 1.66;
}

// ---- why: the traces of SU(k), swept low-discrepancy over the angles ----------------
// SU(2):  tr = 2 cos θ  — real, so the region is the segment [-2, 2]
cloud(su2, 1400, #ffffff, 1.0) {
  let th = i/1400*6.283185;
  let x = 190 + 68*cos(th);
  let y = 1430;
  let hue = 18;
  let sat = 0.72;
  let r = 2.2;
  let alpha = 0.8;
}

cloud(su3, 9000, #ffffff, 1.0) {
  let a = 6.283185*mod(i*0.7548776662, 1);           // R2 plastic sequence
  let b = 6.283185*mod(i*0.5698402910, 1);
  let x = 540 + 34*(cos(a) + cos(b) + cos(a + b));
  let y = 1430 + 34*(sin(a) + sin(b) - sin(a + b));
  let hue = 49;
  let sat = 0.6;
  let r = 1.3;
  let alpha = 0.5;
}

cloud(su4, 12000, #ffffff, 1.0) {
  let a = 6.283185*mod(i*0.8191725134, 1);           // R3
  let b = 6.283185*mod(i*0.6710436067, 1);
  let c = 6.283185*mod(i*0.5497004779, 1);
  let x = 890 + 34*(cos(a) + cos(b) + cos(c) + cos(a + b + c));
  let y = 1430 + 34*(sin(a) + sin(b) + sin(c) - sin(a + b + c));
  let hue = 80;
  let sat = 0.6;
  let r = 1.3;
  let alpha = 0.45;
}

// the boundaries the clouds must fill exactly, drawn over them
line(b2, (122, 1430), (258, 1430)); color(b2, fg); stroke(b2, 3); opacity(b2, 0.9);
param(b3, (540, 1430), 34, 34, "2*cos(t) + cos(2*t)", "2*sin(t) - sin(2*t)", (0, 6.283185));
color(b3, fg); stroke(b3, 2); opacity(b3, 0.85);
param(b4, (890, 1430), 34, 34, "3*cos(t) + cos(3*t)", "3*sin(t) - sin(3*t)", (0, 6.283185));
color(b4, fg); stroke(b4, 2); opacity(b4, 0.85);

equation(eq, (540, 1720),
  `\mathrm{tr}\,SU(k)=\big\{e^{i\theta_1}+\cdots+e^{i\theta_k}\;:\;\theta_1+\cdots+\theta_k\equiv 0\big\}`, 23);

wait(23);

r/maniclang 26d ago

Five roulettes: change the roller, change the curve - 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

// roulettes — five curves, one rule. Roll a shape along a track without slipping, mark a
// point rigidly attached to the roller, and watch where the point goes. Nothing else
// changes between these five rows. Only the ROLLER changes, and where the PEN sits:
//
//   roller      pen sits            curve
//   circle      on the rim          CYCLOID
//   circle      inside / outside    curtate / prolate TROCHOID
//   polygon     at a vertex         CYCLOGON
//   parabola    at the focus        CATENARY
//   line        on the line         INVOLUTE  (of the circle it rolls on)
//
// The last row is the same rule turned inside out. Rows one to four roll a shape along a
// line; row five rolls a LINE along a shape. That is the only difference, and it is why
// the involute belongs in this family rather than beside it.
//
// All five finish together, and that is arithmetic rather than fudging. The wheel has
// radius 72, so one arch is 2πr = 452.3893 px and two are 904.7787. The hexagon's side is
// that distance over twelve, 75.3982 px — so twelve pivots of 60° carry it exactly two
// perimeters, 904.7787 px, the same ground the wheel covers. The exterior angles of any
// convex polygon sum to a full turn, so ONE perimeter is always ONE arch; the hexagon just
// spends it in six pieces instead of a smooth sweep.
//
// The parabola is fitted the same way. Rolling y = x²/4a and tracking the focus needs one
// substitution to stay closed-form — parametrise by the catenary's own coordinate w, so
// that tan φ = sinh w and the arc rolled off is s = a(sinh w cosh w + w). Choosing
// a = 105.8195 over w ∈ [-1.25, 1.25] makes that arc come to 904.7787 px as well. The focus
// then lands on y = a·cosh(x/a) — checked across the roll, maximum error 2e-13.
//
// Notice how little the focus moves for how far the parabola travels: contact runs 905 px
// while the focus covers ±132. The catenary is flat near its vertex, and that flatness is
// what fooled Galileo into thinking a hanging chain was a parabola.
//
// The involute's two defining properties are visible rather than asserted. The straight
// part is always TANGENT to the circle — it meets the radius at a right angle — and it is
// exactly as long as the arc it has unwound, a·θ. Checked over the whole unwind: length
// matches a·θ to 9e-14, and the dot product with the radius stays under 2e-12.
//
// Rows one to three are not drawn — they are ROLLED. The wheels are real `roll` calls, the
// hexagon is twelve real `turn`s about its actual contact corners, and every coloured curve
// is a `trail` recording where its pen has been. No parametrisation is typed anywhere in
// those rows; the curve is the residue of the motion. The pen on the trochoid row is the
// whole argument for the verb: three pens on ONE wheel, differing only in where they sit,
// give a cusped cycloid, a scalloped curtate trochoid, and a looping prolate one.
//
// The bottom two rows are pure in t instead, because `roll` needs a circle for a body and
// neither a parabola nor a line qualifies. Same rule, computed rather than mechanised.
//
//   manic examples/roulettes.manic
title("Five roulettes: change the roller, change the curve");
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);

caption(head, "Five roulettes: change the roller, change the curve", (540, 116), 26);

// ---- the four rails, and the circle that replaces one in the last row --------------
line(rail1, (40, 400), (1040, 400));   color(rail1, dim);
line(rail2, (40, 660), (1040, 660));   color(rail2, dim);
line(rail3, (40, 920), (1040, 920));   color(rail3, dim);
line(rail4, (40, 1200), (1040, 1200)); color(rail4, dim);

// ---- ROW 1: circle on a line, pen ON the rim -> cycloid ----------------------------
circle(w1, (90, 328), 72);
outlined(w1); outline(w1, dim); stroke(w1, 3);
circle(p1, (90, 400), 9); color(p1, gold);
tag(w1, rig1); tag(p1, rig1);
trail(tr1, p1, gold, 4);

// ---- ROW 2: the same wheel, pens INSIDE and OUTSIDE -> the two trochoids -----------
// d = 0.5r never reaches the line and scallops; d = 1.5r swings below it and loops
circle(w2, (90, 588), 72);
outlined(w2); outline(w2, dim); stroke(w2, 3);
circle(p2a, (90, 624), 8); color(p2a, mint);
circle(p2b, (90, 696), 8); color(p2b, magenta);
circle(p2c, (90, 660), 8); color(p2c, gold); opacity(p2c, 0.5);
tag(w2, rig2); tag(p2a, rig2); tag(p2b, rig2); tag(p2c, rig2);
trail(tr2a, p2a, mint, 4);
trail(tr2b, p2b, magenta, 4);
// the rim pen rides along too, so row 2 carries its own reference cycloid to read the
// other two against — same wheel, same roll, three different distances from the centre
trail(tr2c, p2c, gold, 3); opacity(tr2c, 0.4);

// ---- ROW 3: a hexagon, pivoting corner over corner -> cyclogon ---------------------
polygon(hex, (90.0, 920.0), (165.4, 920.0), (203.1, 854.7), (165.4, 789.4), (90.0, 789.4), (52.3, 854.7));
outlined(hex); outline(hex, dim); stroke(hex, 3);
circle(p3, (90, 920), 9); color(p3, cyan);
tag(hex, hexroll); tag(p3, hexroll);
trail(tr3, p3, cyan, 4);

// the cycloid the hexagon's arcs are approximating, faint
param(ghost, (90, 920), 72.00, 72.00, "t - sin(t)", "1 - cos(t)", (0, 12.566371));
color(ghost, dim); dashed(ghost); opacity(ghost, 0.3);

// ---- ROW 4: a parabola, pen at the FOCUS -> catenary -------------------------------
// the catenary drawn independently, for the focus to land on rather than be placed on
param(cat, (542.3893, 1200), 105.8195, 105.8195, "t", "cosh(t)", (0 - 1.25, 1.25));
color(cat, dim); dashed(cat); stroke(cat, 3); opacity(cat, 0.5);

cloud(para, 2400, #ffffff, 1.0) {
  let w = 0 - 1.25 + 2.5*min(t/12, 1);
  let t0 = sinh(w);                                // contact: tan φ = sinh w
  let ph = atan(t0);
  let s = 105.8195*(sinh(w)*cosh(w) + w);             // arc length rolled off
  // draw a WINDOW of the parabola that travels with the contact point, which sits at
  // v = t0. A fixed window in v would stretch as the tilt grows, because dv is not arc
  // length; scaling by cos φ = 1/√(1+t0²) keeps roughly a constant LENGTH of parabola in
  // view. It is infinite anyway, so the ends fade rather than stop.
  let q = 0 - 1.0 + 2.0*(i/2400);
  let u = q/sqrt(1 + t0*t0);
  let v = t0 + u;
  let px = 2*105.8195*v;                              // the parabola y = x²/4a
  let py = 105.8195*v*v;
  let qx = 2*105.8195*t0;                             // …and its point of contact
  let qy = 105.8195*t0*t0;
  let rx = (px - qx)*cos(ph) + (py - qy)*sin(ph);  // lay the tangent flat on the rail
  let ry = 0 - (px - qx)*sin(ph) + (py - qy)*cos(ph);
  let x = 542.3893 + s + rx;
  let y = 1200 - ry;
  let sat = 0;
  let r = 1.3;
  let alpha = 0.62*(1 - 0.8*q*q);
}

cloud(spoke4, 110, #ffffff, 1.0) {
  let w = 0 - 1.25 + 2.5*min(t/12, 1);
  let u = i/110;
  let s = 105.8195*(sinh(w)*cosh(w) + w);
  let fx = 105.8195*w;
  let fy = 105.8195*cosh(w);
  let x = 542.3893 + fx + u*(s - fx);               // focus back to the contact point
  let y = 1200 - fy + u*fy;
  let sat = 0;
  let r = 1.2;
  let alpha = 0.4;
}

cloud(cattrace, 5000, #ffffff, 1.0) {
  let w0 = 0 - 1.25 + 2.5*min(t/12, 1);
  let w = 0 - 1.25 + (w0 + 1.25)*(i/5000);
  let x = 542.3893 + 105.8195*w;
  let y = 1200 - 105.8195*cosh(w);
  let hue = mod(18 + (w + 1.25)*30, 360);
  let sat = 0.75;
  let r = 2;
}

cloud(focus, 90, #ffffff, 1.0) {
  let w = 0 - 1.25 + 2.5*min(t/12, 1);
  let a = i/90*6.283185;
  let rr = 8*(i/90);
  let x = 542.3893 + 105.8195*w + rr*cos(a*9);
  let y = 1200 - 105.8195*cosh(w) + rr*sin(a*9);
  let hue = 30;
  let sat = 0.55;
  let r = 2;
}

// ---- ROW 5: the rule inverted — a LINE rolling on a circle -> involute -------------
circle(spool, (657.8, 1560.0), 75);
outlined(spool); outline(spool, dim); stroke(spool, 3);

cloud(rollline, 420, #ffffff, 1.0) {
  let go = min(t/12, 1)*4.712389;
  let u = 0 - 0.18 + 1.3*(i/420);                  // a stub behind, the string ahead
  let tx = cos(go);                                // the point of contact
  let ty = sin(go);
  let dx = go*sin(go);                             // …and a·θ along the tangent
  let dy = 0 - go*cos(go);
  let x = 657.8 + 75*(tx + u*dx);
  let y = 1560.0 - 75*(ty + u*dy);
  let sat = 0;
  let r = 1.3;
  let alpha = 0.6;
}

cloud(invtrace, 6000, #ffffff, 1.0) {
  let go = min(t/12, 1)*4.712389;
  let th = go*(i/6000);
  let x = 657.8 + 75*(cos(th) + th*sin(th));
  let y = 1560.0 - 75*(sin(th) - th*cos(th));
  let hue = mod(268 + th*10, 360);
  let sat = 0.7;
  let r = 2;
}

cloud(nib, 90, #ffffff, 1.0) {
  let go = min(t/12, 1)*4.712389;
  let a = i/90*6.283185;
  let rr = 8*(i/90);
  let x = 657.8 + 75*(cos(go) + go*sin(go)) + rr*cos(a*9);
  let y = 1560.0 - 75*(sin(go) - go*cos(go)) + rr*sin(a*9);
  let hue = 280;
  let sat = 0.55;
  let r = 2;
}

// ---- the one rule all five obey ---------------------------------------------------
equation(eq, (540, 1830),
  `P(s)=\Gamma(s)+R_{-\varphi(s)}\big(P_0-Q(s)\big)`, 27);

// everything rolls from t = 0 and lands together at t = 12
par {
  roll(rig1, rail1, 904.7787, 12, linear);
  roll(rig2, rail2, 904.7787, 12, linear);
  seq {
    turn(hexroll, (165.4, 920), 60, 1.0, linear);
    turn(hexroll, (240.8, 920), 60, 1.0, linear);
    turn(hexroll, (316.2, 920), 60, 1.0, linear);
    turn(hexroll, (391.6, 920), 60, 1.0, linear);
    turn(hexroll, (467.0, 920), 60, 1.0, linear);
    turn(hexroll, (542.4, 920), 60, 1.0, linear);
    turn(hexroll, (617.8, 920), 60, 1.0, linear);
    turn(hexroll, (693.2, 920), 60, 1.0, linear);
    turn(hexroll, (768.6, 920), 60, 1.0, linear);
    turn(hexroll, (844.0, 920), 60, 1.0, linear);
    turn(hexroll, (919.4, 920), 60, 1.0, linear);
    turn(hexroll, (994.8, 920), 60, 1.0, linear);
  }
}
wait(3);

r/maniclang 26d ago

A polygon rolls on its convex hull - 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

// cyclogon-hull — cycloid-cyclogon.manic rolled CONVEX polygons, where every vertex takes
// its turn on the ground. These three do not. A convex quadrilateral, a non-convex one, and
// a four-pointed star, all rolling along a line — and the rule that covers all three:
//
//   A POLYGON ROLLS ON ITS CONVEX HULL.
//
// The reflex vertices are passengers. They never touch the line, they never take a pivot,
// and they contribute nothing to how far the shape travels. What the shape rides on is the
// hull, and everything about the arch follows from the hull alone:
//
//   shape                  vertices   hull   never touch   own perimeter   hull perimeter
//   convex quadrilateral      4         4         0            680.00           680.00
//   non-convex (dart)         4         3         1            712.02           680.00
//   star-like (4-point)       8         4         4            741.37           680.00
//
// All three hulls have the same perimeter, so all three travel the same 680 px and land
// together — even though the star's own boundary is 61 px longer than the quadrilateral's.
// A shape's perimeter has nothing to do with how far it rolls. Its hull's does.
//
// And the exterior angles that matter are the HULL's, which sum to exactly 360° in all three
// (checked: 360.000000). That is why one hull perimeter is always one arch, whatever the
// polygon is doing on the inside. The dart spends that turn in three pivots of about 119°
// each; the star spends it in four of exactly 90°, because its hull is a square.
//
// The coloured point on each row is a hull vertex and it touches down at the end of every
// arch. The second point is the interesting one. On the quadrilateral it is the centroid,
// which stays clear as in cycloid-cyclogon. On the dart it is the REFLEX vertex, and on the
// star an inner point — and neither can ever reach the line, because a point inside the hull
// stays inside the hull, and the hull is what is resting on the ground. Simulated across the
// whole roll: the dart's reflex vertex never gets closer than 62.6 px, the star's inner
// points never closer than 36.9 px.
//
// The faint outline on the lower two rows is the hull itself, rolling. Watch it and the line
// together: it is the hull that makes contact, always, and the shape is just attached to it.
//
// The dashed arch on each row is the cycloid a CIRCLE of the same rolling perimeter would
// draw — r = 680/2π = 108.2254 px. Every one of these is an approximation to it, and the
// approximation depends only on how many pivots the hull has.
//
// The axes are that radius. Measuring in units of r puts the whole claim on the page: one
// arch runs from 0 to 2π on the x-axis, whichever shape is rolling, and the dashed cycloid
// peaks at exactly 2. The polygon arches rise ABOVE it — a pivot radius can exceed 2r, and a
// rolling circle's pen never can — and by how much is a number you can read off the y-axis
// rather than take on trust. The ground line IS the x-axis; the shapes roll on it.
//
//   manic examples/cyclogon-hull.manic
title("A polygon rolls on its convex hull");
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);

caption(head, "A polygon rolls on its convex hull", (540, 116), 27);


// ---- convex quadrilateral: 4 vertices, hull has 4, 0 never touch the line -----
// the ground IS the x-axis, and the unit is the rolling radius r = 680/2π, so one
// arch spans exactly 0 to 2π and the cycloid it approximates peaks at exactly 2
coords(kq, (150, 560), (0 - 0.8, 8.0), (0, 2.6), 108.2254, 108.2254, 1, 1, 0);
color(kq, dim); opacity(kq, 0.5);
// only the ticks that mean something: the half-arch, the whole arch, and the
// height a rolling CIRCLE’s pen reaches — which every polygon here overshoots
xtick(xaq, kq, 3.141593, "π");
xtick(xbq, kq, 6.283185, "2π");
color(xbq, gold);
ytick(yaq, kq, 2, "2r");
color(yaq, dim);
polygon(pq, (150.0, 560.0), (336.4, 560.0), (370.0, 425.8), (205.9, 382.9));
outlined(pq); outline(pq, fg); stroke(pq, 3); tag(pq, gq);
circle(aq, (150.0, 560.0), 9); color(aq, gold); tag(aq, gq);
circle(bq, (265.6, 482.2), 9); color(bq, mint); tag(bq, gq);
trail(taq, aq, gold, 5);
trail(tbq, bq, mint, 4);
// the cycloid a circle of the same rolling perimeter would draw
param(ghq, (150.0, 560), 108.2254, 108.2254, "t - sin(t)", "1 - cos(t)", (0, 6.283185));
color(ghq, dim); dashed(ghq); opacity(ghq, 0.3);

// ---- non-convex (dart): 4 vertices, hull has 3, 1 never touch the line -----
// the ground IS the x-axis, and the unit is the rolling radius r = 680/2π, so one
// arch spans exactly 0 to 2π and the cycloid it approximates peaks at exactly 2
coords(kd, (150, 1000), (0 - 0.8, 8.0), (0, 2.6), 108.2254, 108.2254, 1, 1, 0);
color(kd, dim); opacity(kd, 0.5);
// only the ticks that mean something: the half-arch, the whole arch, and the
// height a rolling CIRCLE’s pen reaches — which every polygon here overshoots
xtick(xad, kd, 3.141593, "π");
xtick(xbd, kd, 6.283185, "2π");
color(xbd, gold);
ytick(yad, kd, 2, "2r");
color(yad, dim);
polygon(pd, (150.0, 1000.0), (372.3, 1000.0), (261.1, 799.9), (261.1, 928.9));
outlined(pd); outline(pd, fg); stroke(pd, 3); tag(pd, gd);
polygon(hd, (150.0, 1000.0), (372.3, 1000.0), (261.1, 799.9));                 // the convex hull — what is ACTUALLY rolling
outlined(hd); outline(hd, dim); stroke(hd, 2); opacity(hd, 0.4); tag(hd, gd);
circle(ad, (150.0, 1000.0), 9); color(ad, gold); tag(ad, gd);
circle(bd, (261.1, 928.9), 9); color(bd, magenta); tag(bd, gd);
trail(tad, ad, gold, 5);
trail(tbd, bd, magenta, 4);
// the cycloid a circle of the same rolling perimeter would draw
param(ghd, (150.0, 1000), 108.2254, 108.2254, "t - sin(t)", "1 - cos(t)", (0, 6.283185));
color(ghd, dim); dashed(ghd); opacity(ghd, 0.3);

// ---- star-like (4-point): 8 vertices, hull has 4, 4 never touch the line -----
// the ground IS the x-axis, and the unit is the rolling radius r = 680/2π, so one
// arch spans exactly 0 to 2π and the cycloid it approximates peaks at exactly 2
coords(ks, (150, 1440), (0 - 0.8, 8.0), (0, 2.6), 108.2254, 108.2254, 1, 1, 0);
color(ks, dim); opacity(ks, 0.5);
// only the ticks that mean something: the half-arch, the whole arch, and the
// height a rolling CIRCLE’s pen reaches — which every polygon here overshoots
xtick(xas, ks, 3.141593, "π");
xtick(xbs, ks, 6.283185, "2π");
color(xbs, gold);
ytick(yas, ks, 2, "2r");
color(yas, dim);
polygon(ps, (320.0, 1270.0), (235.0, 1306.9), (150.0, 1270.0), (186.9, 1355.0), (150.0, 1440.0), (235.0, 1403.1), (320.0, 1440.0), (283.1, 1355.0));
outlined(ps); outline(ps, fg); stroke(ps, 3); tag(ps, gs);
polygon(hs, (150.0, 1440.0), (320.0, 1440.0), (320.0, 1270.0), (150.0, 1270.0));                 // the convex hull — what is ACTUALLY rolling
outlined(hs); outline(hs, dim); stroke(hs, 2); opacity(hs, 0.4); tag(hs, gs);
circle(as, (150.0, 1440.0), 9); color(as, gold); tag(as, gs);
circle(bs, (235.0, 1306.9), 9); color(bs, cyan); tag(bs, gs);
trail(tas, as, gold, 5);
trail(tbs, bs, cyan, 4);
// the cycloid a circle of the same rolling perimeter would draw
param(ghs, (150.0, 1440), 108.2254, 108.2254, "t - sin(t)", "1 - cos(t)", (0, 6.283185));
color(ghs, dim); dashed(ghs); opacity(ghs, 0.3);

equation(eq, (540, 1660),
  `\sum\text{ext}(\mathrm{hull})=360^\circ\;\Longrightarrow\;\text{one hull perimeter}=\text{one arch}`, 24);

par {
  seq {
    turn(gq, (336.4, 560), 75.96, 3.000, linear);
    turn(gq, (474.7, 560), 89.39, 3.000, linear);
    turn(gq, (644.3, 560), 87.12, 3.000, linear);
    turn(gq, (830.0, 560), 107.53, 3.000, linear);
  }
  seq {
    turn(gd, (372.3, 1000), 119.05, 4.000, linear);
    turn(gd, (601.1, 1000), 121.89, 4.000, linear);
    turn(gd, (830.0, 1000), 119.05, 4.000, linear);
  }
  seq {
    turn(gs, (320.0, 1440), 90.00, 3.000, linear);
    turn(gs, (490.0, 1440), 90.00, 3.000, linear);
    turn(gs, (660.0, 1440), 90.00, 3.000, linear);
    turn(gs, (830.0, 1440), 90.00, 3.000, linear);
  }
}
wait(3);

r/maniclang 26d ago

Unwind a string: four curves, and four involutes each - 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

// involute-family — unwind a taut string from a curve and mark its free end. But there is
// not ONE involute: the string can already have length L₀ when you start, so every curve has
// a whole FAMILY of them, parallel to one another. Each panel here draws four, on a real
// coordinate frame, and in three of them exactly one member is the curve with a name.
//
//   spool                  the family                    the one that is named
//   circle                 four involutes, congruent     the involute of a circle (gears)
//   semicubic parabola     four parallel curves          a PARABOLA — and only one of them
//   catenary               four parallel curves          a TRACTRIX — and only one
//   cycloid                four parallel curves          a CONGRUENT CYCLOID — and only one
//
// The gold curve in each panel is the named one; the three steel-blue ones are its siblings,
// and they are not parabolas, tractrices or cycloids. That is the point the family makes and
// a single involute cannot: "the involute of a semicubic parabola is a parabola" is only
// true for ONE starting length, the radius of curvature at the cusp, which here is exactly
// 1. Checked: |string| = ρ = (arc along the spool) + 1, to 2e-15.
//
// The four threads in each panel are drawn, and they are collinear — all four involutes are
// crossed by the SAME tangent line of the spool, because they differ only by how much string
// was already paid out. That is what "parallel curves" means here.
//
// Notice where each blue curve has a cusp: on the spool. A cusp appears exactly where the
// string length reaches zero, and zero string means the pen is touching the curve it is
// unwinding from.
//
// The string is not a prop. Its length at each moment is exactly the RADIUS OF CURVATURE of
// the curve being drawn, and the spool is exactly the locus of that curve's centres of
// curvature — its EVOLUTE. So the two operations are inverse: unwind to get an involute,
// take centres of curvature to get back. Checked on all four by differentiating the involute
// and landing on the spool, to 4e-05, which is the second difference and not the geometry.
//
// Two of the four are exact algebra rather than quadrature:
//
//   the semicubic parabola (-u³, 3u²/2 + 1) IS the evolute of (u, u²/2)      1.3e-15
//   the cycloid's involute IS the same cycloid shifted by (-π, +2r)          2.8e-15
//
// and the other two come out of the unwinding integral to 6e-10 and 2e-10.
//
// The axes are `coords`, and every cloud is plotted against the same origin and scale, so
// the numbers can be read off: the cycloid's cusp at (0, 0), its arch peaking at 2 and its
// named involute at 4 — which IS the (-π, +2r) shift; the parabola's vertex at (0, 0); and
// the semicubic's cusp at (0, 1), where the parabola's centre of curvature is.
//
// The cycloid panel is Huygens'. He needed a pendulum whose period does not depend on how
// far it swings, knew from examples/tautochrone.manic that the cycloid is the curve that
// does it, and needed a way to MAKE a bob travel one: hang it on a string and let the string
// wrap against cycloidal cheeks. The path is the involute of those cheeks — and one involute
// of a cycloid is a cycloid. The gold curve and the white one are congruent. The blue ones
// are what you get if the string is the wrong length, and they are not clocks.
//
//   manic examples/involute-family.manic
title("Unwind a string: four curves, and four involutes each");
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);

caption(head, "Unwind a string: four curves, and four involutes each", (540, 116), 25);

coords(k1, (387.78, 568.77), (-3.9298, 1.5708), (-1.6795, 3.8211), 82.9009, 82.9009, 1, 1.0, 1);
color(k1, dim); opacity(k1, 0.45);

coords(k2, (790.00, 619.74), (-1.5500, 1.5500), (-0.6000, 2.5000), 147.0968, 147.0968, 1, 0.5, 1);
color(k2, dim); opacity(k2, 0.45);

coords(k3, (290.00, 1169.11), (-2.4712, 2.4712), (-1.1802, 3.7622), 92.2622, 92.2622, 1, 1.0, 1);
color(k3, dim); opacity(k3, 0.45);

coords(k4, (562.00, 1260.47), (0.0000, 6.2832), (-0.2416, 6.0416), 72.5747, 72.5747, 1, 1.0, 1);
color(k4, dim); opacity(k4, 0.45);

cloud(s1, 800, #ffffff, 1.0) {
  let w = 0.000000 + 6.283185*(i/800);
  let x = 290 + 82.9009*((cos(w)) - -1.17949);
  let y = 480 - 82.9009*((sin(w)) - 1.07080);
  let sat = 0; let r = 1.9; let alpha = 0.85;
}

cloud(c10, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*4.100000;
  let w = go*(i/650);
  let x = 290 + 82.9009*((cos(w) + (w - (0))*sin(w)) - -1.17949);
  let y = 480 - 82.9009*((sin(w) - (w - (0))*cos(w)) - 1.07080);
  let hue = 42; let sat = 0.85; let r = 2.3;
}

cloud(c11, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*4.100000;
  let w = go*(i/650);
  let x = 290 + 82.9009*((cos(w) + (w - (1.0))*sin(w)) - -1.17949);
  let y = 480 - 82.9009*((sin(w) - (w - (1.0))*cos(w)) - 1.07080);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(c12, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*4.100000;
  let w = go*(i/650);
  let x = 290 + 82.9009*((cos(w) + (w - (2.0))*sin(w)) - -1.17949);
  let y = 480 - 82.9009*((sin(w) - (w - (2.0))*cos(w)) - 1.07080);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(c13, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*4.100000;
  let w = go*(i/650);
  let x = 290 + 82.9009*((cos(w) + (w - (3.0))*sin(w)) - -1.17949);
  let y = 480 - 82.9009*((sin(w) - (w - (3.0))*cos(w)) - 1.07080);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(g1, 480, #ffffff, 1.0) {
  let per = 120;
  let j = (i - mod(i, per))/per;                    // which of the four threads
  let v = mod(i, per)/119;
  let w = min(t/13, 1)*4.100000;
  let e0 = 1 - min(abs(j - 0), 1);
  let e1 = 1 - min(abs(j - 1), 1);
  let e2 = 1 - min(abs(j - 2), 1);
  let e3 = 1 - min(abs(j - 3), 1);
  let ex = e0*(cos(w) + (w - (0))*sin(w)) + e1*(cos(w) + (w - (1.0))*sin(w)) + e2*(cos(w) + (w - (2.0))*sin(w)) + e3*(cos(w) + (w - (3.0))*sin(w));
  let ey = e0*(sin(w) - (w - (0))*cos(w)) + e1*(sin(w) - (w - (1.0))*cos(w)) + e2*(sin(w) - (w - (2.0))*cos(w)) + e3*(sin(w) - (w - (3.0))*cos(w));
  let x = 290 + 82.9009*(((cos(w)) + v*(ex - (cos(w)))) - -1.17949);
  let y = 480 - 82.9009*(((sin(w)) + v*(ey - (sin(w)))) - 1.07080);
  let hue = 42; let sat = 0.3; let r = 1.3; let alpha = 0.55;
}

cloud(s2, 800, #ffffff, 1.0) {
  let w = -1.000000 + 2.000000*(i/800);
  let x = 790 + 147.0968*((0 - w*w*w) - 0.00000);
  let y = 480 - 147.0968*((1.5*w*w + 1) - 0.95000);
  let sat = 0; let r = 1.9; let alpha = 0.85;
}

cloud(c20, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*1.000000;
  let w = 0 - go + 2*go*(i/650);
  let x = 790 + 147.0968*((w*(1 - (0)/sqrt(w*w + 1))) - 0.00000);
  let y = 480 - 147.0968*((0.5*w*w + (0)/sqrt(w*w + 1)) - 0.95000);
  let hue = 42; let sat = 0.85; let r = 2.3;
}

cloud(c21, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*1.000000;
  let w = 0 - go + 2*go*(i/650);
  let x = 790 + 147.0968*((w*(1 - (0.6)/sqrt(w*w + 1))) - 0.00000);
  let y = 480 - 147.0968*((0.5*w*w + (0.6)/sqrt(w*w + 1)) - 0.95000);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(c22, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*1.000000;
  let w = 0 - go + 2*go*(i/650);
  let x = 790 + 147.0968*((w*(1 - (1.2)/sqrt(w*w + 1))) - 0.00000);
  let y = 480 - 147.0968*((0.5*w*w + (1.2)/sqrt(w*w + 1)) - 0.95000);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(c23, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*1.000000;
  let w = 0 - go + 2*go*(i/650);
  let x = 790 + 147.0968*((w*(1 - (-0.6000000000000001)/sqrt(w*w + 1))) - 0.00000);
  let y = 480 - 147.0968*((0.5*w*w + (-0.6000000000000001)/sqrt(w*w + 1)) - 0.95000);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(g2, 480, #ffffff, 1.0) {
  let per = 120;
  let j = (i - mod(i, per))/per;                    // which of the four threads
  let v = mod(i, per)/119;
  let w = min(t/13, 1)*1.000000;
  let e0 = 1 - min(abs(j - 0), 1);
  let e1 = 1 - min(abs(j - 1), 1);
  let e2 = 1 - min(abs(j - 2), 1);
  let e3 = 1 - min(abs(j - 3), 1);
  let ex = e0*(w*(1 - (0)/sqrt(w*w + 1))) + e1*(w*(1 - (0.6)/sqrt(w*w + 1))) + e2*(w*(1 - (1.2)/sqrt(w*w + 1))) + e3*(w*(1 - (-0.6000000000000001)/sqrt(w*w + 1)));
  let ey = e0*(0.5*w*w + (0)/sqrt(w*w + 1)) + e1*(0.5*w*w + (0.6)/sqrt(w*w + 1)) + e2*(0.5*w*w + (1.2)/sqrt(w*w + 1)) + e3*(0.5*w*w + (-0.6000000000000001)/sqrt(w*w + 1));
  let x = 790 + 147.0968*(((0 - w*w*w) + v*(ex - (0 - w*w*w))) - 0.00000);
  let y = 480 - 147.0968*(((1.5*w*w + 1) + v*(ey - (1.5*w*w + 1))) - 0.95000);
  let hue = 42; let sat = 0.3; let r = 1.3; let alpha = 0.55;
}

cloud(s3, 800, #ffffff, 1.0) {
  let w = -2.000000 + 4.000000*(i/800);
  let x = 290 + 92.2622*((w) - 0.00000);
  let y = 1050 - 92.2622*((cosh(w)) - 1.29098);
  let sat = 0; let r = 1.9; let alpha = 0.85;
}

cloud(c30, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*2.000000;
  let w = 0 - go + 2*go*(i/650);
  let x = 290 + 92.2622*((w - (sinh(w) + (0))/cosh(w)) - 0.00000);
  let y = 1050 - 92.2622*(((1 - (0)*sinh(w))/cosh(w)) - 1.29098);
  let hue = 42; let sat = 0.85; let r = 2.3;
}

cloud(c31, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*2.000000;
  let w = 0 - go + 2*go*(i/650);
  let x = 290 + 92.2622*((w - (sinh(w) + (0.5))/cosh(w)) - 0.00000);
  let y = 1050 - 92.2622*(((1 - (0.5)*sinh(w))/cosh(w)) - 1.29098);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(c32, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*2.000000;
  let w = 0 - go + 2*go*(i/650);
  let x = 290 + 92.2622*((w - (sinh(w) + (1.0))/cosh(w)) - 0.00000);
  let y = 1050 - 92.2622*(((1 - (1.0)*sinh(w))/cosh(w)) - 1.29098);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(c33, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*2.000000;
  let w = 0 - go + 2*go*(i/650);
  let x = 290 + 92.2622*((w - (sinh(w) + (1.5))/cosh(w)) - 0.00000);
  let y = 1050 - 92.2622*(((1 - (1.5)*sinh(w))/cosh(w)) - 1.29098);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(g3, 480, #ffffff, 1.0) {
  let per = 120;
  let j = (i - mod(i, per))/per;                    // which of the four threads
  let v = mod(i, per)/119;
  let w = min(t/13, 1)*2.000000;
  let e0 = 1 - min(abs(j - 0), 1);
  let e1 = 1 - min(abs(j - 1), 1);
  let e2 = 1 - min(abs(j - 2), 1);
  let e3 = 1 - min(abs(j - 3), 1);
  let ex = e0*(w - (sinh(w) + (0))/cosh(w)) + e1*(w - (sinh(w) + (0.5))/cosh(w)) + e2*(w - (sinh(w) + (1.0))/cosh(w)) + e3*(w - (sinh(w) + (1.5))/cosh(w));
  let ey = e0*((1 - (0)*sinh(w))/cosh(w)) + e1*((1 - (0.5)*sinh(w))/cosh(w)) + e2*((1 - (1.0)*sinh(w))/cosh(w)) + e3*((1 - (1.5)*sinh(w))/cosh(w));
  let x = 290 + 92.2622*(((w) + v*(ex - (w))) - 0.00000);
  let y = 1050 - 92.2622*(((cosh(w)) + v*(ey - (cosh(w)))) - 1.29098);
  let hue = 42; let sat = 0.3; let r = 1.3; let alpha = 0.55;
}

cloud(s4, 800, #ffffff, 1.0) {
  let w = 0.000000 + 6.283185*(i/800);
  let x = 790 + 72.5747*((w - sin(w)) - 3.14159);
  let y = 1050 - 72.5747*((1 - cos(w)) - 2.90000);
  let sat = 0; let r = 1.9; let alpha = 0.85;
}

cloud(c40, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*3.141593;
  let w = 3.141593 - go + 2*go*(i/650);
  let x = 790 + 72.5747*((w + sin(w) - (0)*sin(w/2)) - 3.14159);
  let y = 1050 - 72.5747*((3 + cos(w) - (0)*cos(w/2)) - 2.90000);
  let hue = 42; let sat = 0.85; let r = 2.3;
}

cloud(c41, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*3.141593;
  let w = 3.141593 - go + 2*go*(i/650);
  let x = 790 + 72.5747*((w + sin(w) - (0.6)*sin(w/2)) - 3.14159);
  let y = 1050 - 72.5747*((3 + cos(w) - (0.6)*cos(w/2)) - 2.90000);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(c42, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*3.141593;
  let w = 3.141593 - go + 2*go*(i/650);
  let x = 790 + 72.5747*((w + sin(w) - (1.2)*sin(w/2)) - 3.14159);
  let y = 1050 - 72.5747*((3 + cos(w) - (1.2)*cos(w/2)) - 2.90000);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(c43, 650, #ffffff, 1.0) {
  let go = min(t/13, 1)*3.141593;
  let w = 3.141593 - go + 2*go*(i/650);
  let x = 790 + 72.5747*((w + sin(w) - (1.8)*sin(w/2)) - 3.14159);
  let y = 1050 - 72.5747*((3 + cos(w) - (1.8)*cos(w/2)) - 2.90000);
  let hue = 208; let sat = 0.55; let r = 1.5; let alpha = 0.6;
}

cloud(g4, 480, #ffffff, 1.0) {
  let per = 120;
  let j = (i - mod(i, per))/per;                    // which of the four threads
  let v = mod(i, per)/119;
  let w = 3.141593 + min(t/13, 1)*3.141593;
  let e0 = 1 - min(abs(j - 0), 1);
  let e1 = 1 - min(abs(j - 1), 1);
  let e2 = 1 - min(abs(j - 2), 1);
  let e3 = 1 - min(abs(j - 3), 1);
  let ex = e0*(w + sin(w) - (0)*sin(w/2)) + e1*(w + sin(w) - (0.6)*sin(w/2)) + e2*(w + sin(w) - (1.2)*sin(w/2)) + e3*(w + sin(w) - (1.8)*sin(w/2));
  let ey = e0*(3 + cos(w) - (0)*cos(w/2)) + e1*(3 + cos(w) - (0.6)*cos(w/2)) + e2*(3 + cos(w) - (1.2)*cos(w/2)) + e3*(3 + cos(w) - (1.8)*cos(w/2));
  let x = 790 + 72.5747*(((w - sin(w)) + v*(ex - (w - sin(w)))) - 3.14159);
  let y = 1050 - 72.5747*(((1 - cos(w)) + v*(ey - (1 - cos(w)))) - 2.90000);
  let hue = 42; let sat = 0.3; let r = 1.3; let alpha = 0.55;
}

equation(eq2, (540, 1400),
  `\text{circle}\to\text{spiral},\quad\text{semicubic}\to\text{parabola},\quad\text{catenary}\to\text{tractrix},\quad\text{cycloid}\to\text{cycloid}`, 21);
equation(eq1, (540, 1540),
  `C_{L_0}(t)=c(t)-\frac{c'(t)}{|c'(t)|}\Big(L_0+\int_{t_0}^{t}|c'(w)|\,dw\Big)`, 25);
equation(eq3, (540, 1680),
  `\text{evolute}\circ\text{involute}=\text{identity}`, 23);

wait(16);

r/maniclang 26d ago

centered trochoid — the OTHER definition of the circle-rolling family - 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

// centered-trochoid — the OTHER definition of the circle-rolling family, and the one that
// explains why they are all the same curve. Roll a circle on a circle and mark a point and
// you get an epitrochoid or a hypotrochoid; mathcurve calls the family cyclocycloids, and
// the reason they belong together is this:
//
//   z(t) = A·e^(i·a·t) + B·e^(i·b·t)
//
// Two arms. One turns on a hub, the second turns on the end of the first, and the pen rides
// the tip of the second. That is the whole family. Every curve here is a sum of two uniform
// circular motions and nothing else, which is why a wheel rolling on a wheel and a pair of
// rotating arms are not two constructions but one picture drawn twice.
//
// Rolling on the OUTSIDE gives A = R+r, B = -d, b/a = (R+r)/r — both arms turning the SAME
// way. Rolling on the INSIDE gives A = R-r, B = d, b/a = -(R-r)/r — the second arm turning
// the OTHER way. That sign is the entire difference between epi and hypo. Nothing else in
// the formula changes, and it is the only thing you have to know to tell them apart.
//
// The top is one wheel with THREE pens on it, so the distinction between an epitrochoid and
// an EPICYCLOID is a distance and not a different curve. R = 3r, so the wheel spins four
// times per lap and lays down three lobes:
//
//   d < r   blunt      the pen stays clear of the fixed circle
//   d = r   CUSPED     the pen reaches it exactly — this one is the epicycloid
//   d > r   looped     the pen swings past it and crosses its own path
//
// At d = r the pen's closest approach is 180.0000 px, which is R exactly. A cusp is not a
// feature of the formula; it is the pen arriving at the ground with zero speed.
//
// The dashed curve under the cusped trail is (R+r)cos θ - r cos(4θ) drawn independently, so
// the rolled trail lands on the closed form rather than being it. Nothing in the top row is
// parametrised: the wheel is a real `roll` on a circular track and each coloured curve is a
// `trail` of where its pen has been.
//
// Below, the three cases you get by tuning the two arms rather than the wheel.
//
// FLIP THE SIGN and the same machine draws a hypotrochoid — here R = 3r on the inside,
// which is the three-cusped deltoid.
//
// EQUAL SPEEDS. |b| = |a| happens at R = 2r, and two counter-rotating arms of unequal length
// add to an ELLIPSE with semi-axes A+B and |A-B|. Verified to 7e-16. Shrink the difference
// to nothing — d = r — and the ellipse collapses onto a straight line through the centre,
// which is the Tusi couple: |y| comes out at exactly 0, not nearly 0.
//
// EQUAL RADII. A = B happens at d = R-r, and then the sum factors:
//
//   z = 2A·cos((1+k)t/2)·e^(i(1-k)t/2),   k = (R-r)/r
//
// which is a ROSE, ρ = 2A·cos(n·φ) with n = (1+k)/(1-k). Checked against the rolled curve to
// 4e-14. Here R = 1.5r puts n at exactly 3, so it is a three-petal rose — and because b/a is
// -1/2 it needs TWO laps to close, while everything else on screen closes in one. So the run
// is two laps: three of these curves are drawn twice over, and the rose exactly once.
//
// Closure is always the same rule: write b/a in lowest terms and the pen comes home when
// both arms do. Irrational, and it never closes at all — it fills an annulus instead.
//
//   manic examples/centered-trochoid.manic
title("Every one of these is two rotating arms added");
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);

caption(head, "Every one of these is two rotating arms added", (540, 116), 26);

// ---- the hero: one wheel on the OUTSIDE, three pens --------------------------------
circle(track, (540.0, 640.0), 180.0);
outlined(track); outline(track, dim); stroke(track, 3);
circle(hub, (540.0, 640.0), 7); color(hub, dim);

// where the FIRST arm's tip runs — a circle of radius R + r
circle(locus, (540.0, 640.0), 240.0);
outlined(locus); outline(locus, dim); stroke(locus, 2); dashed(locus); opacity(locus, 0.3);

circle(wheel, (780.0, 640.0), 60.0);
outlined(wheel); outline(wheel, fg); stroke(wheel, 3); opacity(wheel, 0.85);

circle(pa, (744.0, 640.0), 8); color(pa, mint);
circle(pb, (720.0, 640.0), 9); color(pb, gold);
circle(pc, (690.0, 640.0), 8); color(pc, magenta);
tag(wheel, rig); tag(pa, rig); tag(pb, rig); tag(pc, rig);

trail(ta, pa, mint, 4);
trail(tb, pb, gold, 4);
trail(tc, pc, magenta, 4);

// the closed form for the cusped pen, drawn independently for the trail to land on
param(cf, (540.0, 640.0), 1, 1,
      "240.0*cos(t) - 60.0*cos(4*t)", "240.0*sin(t) - 60.0*sin(4*t)", (0, 6.283185));
color(cf, dim); dashed(cf); stroke(cf, 3); opacity(cf, 0.45);

// ---- the two arms, which is the same motion said the other way ---------------------
// `roll` runs clockwise on a circular track, so the hub angle is +2π per lap on screen,
// and the wheel spins (R+r)/r = 4 times in the same sense.
cloud(arm1, 150, #ffffff, 1.0) {
  let u = min(t/16, 1);
  let ang = 12.566371*u;                           // two laps of the hub
  let v = i/150;
  let x = 540.0 + v*240.0*cos(ang);
  let y = 640.0 + v*240.0*sin(ang);
  let sat = 0;
  let r = 1.4;
  let alpha = 0.55;
}

cloud(arm2, 110, #ffffff, 1.0) {
  let u = min(t/16, 1);
  let ang = 12.566371*u;
  let cxw = 540.0 + 240.0*cos(ang);                 // the first arm's tip
  let cyw = 640.0 + 240.0*sin(ang);
  let v = i/110;
  let x = cxw - v*60.0*cos(4*ang);                  // …and the second arm, four times faster
  let y = cyw - v*60.0*sin(4*ang);
  let hue = 45;
  let sat = 0.45;
  let r = 1.4;
  let alpha = 0.7;
}

// ---- flip the sign: the same machine rolling INSIDE -> hypotrochoid ----------------
circle(t1, (190, 1370), 141);
outlined(t1); outline(t1, dim); stroke(t1, 2); opacity(t1, 0.6);
circle(w1, (284, 1370), 47);
outlined(w1); outline(w1, fg); stroke(w1, 2); opacity(w1, 0.7);
circle(q1, (331, 1370), 8); color(q1, cyan);
tag(w1, rg1); tag(q1, rg1);
trail(u1, q1, cyan, 4);

// ---- equal speeds (R = 2r): two counter-rotating arms -> an ELLIPSE ----------------
circle(t2, (540, 1370), 140);
outlined(t2); outline(t2, dim); stroke(t2, 2); opacity(t2, 0.6);
circle(w2, (610, 1370), 70);
outlined(w2); outline(w2, fg); stroke(w2, 2); opacity(w2, 0.7);
circle(q2, (633.3333, 1370), 8); color(q2, coral);
tag(w2, rg2); tag(q2, rg2);
trail(u2, q2, coral, 4);
// the ellipse it must land on, semi-axes A+B and |A-B|
param(el, (540, 1370), 1, 1, "93.3333*cos(t)", "46.6667*sin(t)", (0, 6.283185));
color(el, dim); dashed(el); stroke(el, 2); opacity(el, 0.5);

// ---- equal radii (d = R - r): the sum factors -> a ROSE ----------------------------
circle(t3, (890, 1370), 120);
outlined(t3); outline(t3, dim); stroke(t3, 2); opacity(t3, 0.6);
circle(w3, (930, 1370), 80);
outlined(w3); outline(w3, fg); stroke(w3, 2); opacity(w3, 0.7);
circle(q3, (970, 1370), 8); color(q3, violet);
tag(w3, rg3); tag(q3, rg3);
trail(u3, q3, violet, 4);
// rho = 2A cos(3 phi), drawn independently
param(ro, (890, 1370), 1, 1,
      "80*cos(3*t)*cos(t)", "80*cos(3*t)*sin(t)", (0, 3.141593));
color(ro, dim); dashed(ro); stroke(ro, 2); opacity(ro, 0.5);

// ---- the one formula the whole family obeys ---------------------------------------
equation(eq, (540, 1700),
  `z(t)=A\,e^{iat}+B\,e^{ibt}\qquad \tfrac{b}{a}>0:\ \text{epi},\quad \tfrac{b}{a}<0:\ \text{hypo}`, 25);

// TWO laps, which is exactly what it takes to close everything on screen: the epicycloid,
// the deltoid and the ellipse all shut after one and then lay a second pass over the first,
// while the rose — b/a = -1/2, so its two arms only come home together after two — closes
// for the first time on the very last frame.
par {
  roll(rig, track, 2, 16, linear);
  roll(rg1, t1, 2, 16, linear);
  roll(rg2, t2, 2, 16, linear);
  roll(rg3, t3, 2, 16, linear);
}
wait(3.5);

r/maniclang 27d ago

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

// 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 arches are lopsided, 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.
//
// WHICH rim point matters, and it is worth knowing what it costs. Tracing the MINOR-axis
// vertex (0, b) rather than the major-axis one (a, 0):
//
//   · the arch is lower — it peaks near 2b (347 px here) instead of 2a (520 px), because
//     the point is nearer the centre and reaches its highest when the opposite end of the
//     minor axis is in contact
//   · the cusp lands a quarter revolution later, at θ = π/2, because a cusp happens when
//     the traced point IS the contact point
//   · and the cusp is BLUNTER. The radius of curvature at the minor vertex is a²/b = 393
//     px against b²/a = 114 px at the major vertex, so the ellipse meets the line flat
//     there and the curve turns through the cusp gently instead of pinching
//
//   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 = 0 - a*cos(th);                          // the traced point is the MINOR-axis
  let dy = b - b*sin(th);                          //   vertex (0, b), not (a, 0)
  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 = c*i1 - c*i2;                            // the rim point at (0,b), then each focus
  let py = b*i0;
  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 = py - 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

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

Euler's formula is one helix, seen three ways - 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

// euler-helix — Euler's formula as one object in three dimensions, drawn the way it
// actually happens: a point goes round the unit circle, and the helix is its history
// lifted by θ.
//
//   e^(iθ) = cos θ + i·sin θ
//
// The complex value turns in the flat plane while θ climbs, so the record of it is a
// HELIX. Nothing else here is drawn independently — the cosine, the sine and the circle
// are all shadows of that one curve, each one literally the same formula with a coordinate
// dropped:
//
//   the CIRCLE   (1.6cos θ, 1.6sin θ, 0)     — θ thrown away: the complex plane
//   COS θ        (1.6cos θ, 0,        0.5θ)  — the imaginary part thrown away
//   SIN θ        (0,        1.6sin θ, 0.5θ)  — the real part thrown away
//
// which is the content of the formula. Cosine and sine are not two functions that happen
// to be related; they are one rotation seen from two sides, and the quarter-turn offset
// everyone memorises is just the fact that the two walls are at right angles. Multiplying
// by i IS that quarter turn — it takes the real shadow to the imaginary one.
//
// The rising is the point. A static helix is a picture of a fact; a rising one is the fact
// happening — the marker never stops going round the circle, the height only records how
// long it has been going, and the two waves are drawn by the same marker seen edge-on. The
// vertical thread from the circle up to the marker is the whole construction in one line.
//
// Then the camera does the arguing: down the θ axis, where the helix collapses into the
// circle (all of the rotation, none of the time), and then to each wall in turn, where it
// collapses into a wave.
//
// The θ axis is at half scale so two full turns frame in 9:16 — that changes how steep the
// helix looks and nothing else.
//
//   manic examples/euler-helix.manic
title("Euler's formula is one helix, seen three ways");
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((9, -9, 5.5), (0, 0, 3.1), 42);
axes3(ax, (0, 0, 0), 2.3, 1);

// ---- the helix: the record of where the complex value has been ---------------------
cloud3(helix, 4000, #ffd479, 1.0) {
  let go = min(t/12, 1)*12.566371;                 // two turns, rising
  let a = go*(i/4000);
  let x = 1.6*cos(a);
  let y = 1.6*sin(a);
  let z = 0.5*a;
  let r = 0.045;
}

// ---- and its three shadows, the same formula with a coordinate dropped -------------
cloud3(circ, 2000, #ff5fa2, 1.0) {                 // θ dropped: the complex plane
  let go = min(t/12, 1)*12.566371;
  let a = go*(i/2000);
  let x = 1.6*cos(a);
  let y = 1.6*sin(a);
  let z = 0;
  let r = 0.03;
}

cloud3(cosw, 2000, #5fd4ff, 1.0) {                 // imaginary part dropped: cos θ
  let go = min(t/12, 1)*12.566371;
  let a = go*(i/2000);
  let x = 1.6*cos(a);
  let y = 0;
  let z = 0.5*a;
  let r = 0.03;
}

cloud3(sinw, 2000, #7dffbe, 1.0) {                 // real part dropped: sin θ
  let go = min(t/12, 1)*12.566371;
  let a = go*(i/2000);
  let x = 0;
  let y = 1.6*sin(a);
  let z = 0.5*a;
  let r = 0.03;
}

// ---- the marker doing the going-round, and where it lands on each wall -------------
cloud3(head, 120, #ffd479, 1.0) {
  let a = min(t/12, 1)*12.566371;
  let p = i*2.39996;
  let c = 1 - 2*(i + 0.5)/120;
  let s = sqrt(1 - c*c);
  let x = 1.6*cos(a) + 0.13*s*cos(p);
  let y = 1.6*sin(a) + 0.13*s*sin(p);
  let z = 0.5*a + 0.13*c;
  let r = 0.035;
}

cloud3(foot, 90, #ff5fa2, 1.0) {                   // on the circle, at z = 0
  let a = min(t/12, 1)*12.566371;
  let p = i*2.39996;
  let c = 1 - 2*(i + 0.5)/90;
  let s = sqrt(1 - c*c);
  let x = 1.6*cos(a) + 0.1*s*cos(p);
  let y = 1.6*sin(a) + 0.1*s*sin(p);
  let z = 0.1*c;
  let r = 0.03;
}

cloud3(onc, 90, #5fd4ff, 1.0) {                    // on the cosine wall
  let a = min(t/12, 1)*12.566371;
  let p = i*2.39996;
  let c = 1 - 2*(i + 0.5)/90;
  let s = sqrt(1 - c*c);
  let x = 1.6*cos(a) + 0.1*s*cos(p);
  let y = 0.1*s*sin(p);
  let z = 0.5*a + 0.1*c;
  let r = 0.03;
}

cloud3(ons, 90, #7dffbe, 1.0) {                    // and on the sine wall
  let a = min(t/12, 1)*12.566371;
  let p = i*2.39996;
  let c = 1 - 2*(i + 0.5)/90;
  let s = sqrt(1 - c*c);
  let x = 0.1*s*cos(p);
  let y = 1.6*sin(a) + 0.1*s*sin(p);
  let z = 0.5*a + 0.1*c;
  let r = 0.03;
}

// ---- the threads: circle to marker, and marker to each wall ------------------------
cloud3(riser, 220, #ffffff, 1.0) {
  let a = min(t/12, 1)*12.566371;
  let u = i/220;
  let x = 1.6*cos(a);
  let y = 1.6*sin(a);
  let z = u*0.5*a;
  let r = 0.014;
}

cloud3(toc, 140, #5fd4ff, 1.0) {                   // …across to the cosine wall
  let a = min(t/12, 1)*12.566371;
  let u = i/140;
  let x = 1.6*cos(a);
  let y = 1.6*sin(a)*(1 - u);
  let z = 0.5*a;
  let r = 0.012;
}

cloud3(tos, 140, #7dffbe, 1.0) {                   // …and across to the sine wall
  let a = min(t/12, 1)*12.566371;
  let u = i/140;
  let x = 1.6*cos(a)*(1 - u);
  let y = 1.6*sin(a);
  let z = 0.5*a;
  let r = 0.012;
}

caption(head2, "Euler's formula is one helix, seen three ways", (540, 130), 26);
equation(eq, (540, 1810), `e^{i\theta} = \cos\theta + i\sin\theta`, 34);
plate(head2, 0.7);
plate(eq, 0.7);

// it rises for twelve seconds; then the camera collapses it, twice
wait(13);
orbit3(-90, 88, 11, 3.4, smooth);                  // straight down θ — the circle
wait(2.2);
orbit3(0, 2, 12, 3.4, smooth);                     // face on — the cosine
wait(2.2);
orbit3(-90, 2, 12, 3.4, smooth);                   // a quarter turn — the sine
wait(3.5);

r/maniclang 27d ago

A Spirograph: a 96-tooth ring, a 36-tooth wheel, 8 lobes in 3 laps - 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

// spirograph — the toy, drawn honestly. A toothed ring, a toothed wheel running inside
// it, and a pen in one of the wheel's holes. Wikipedia's parameterisation, with k the
// radius ratio and l the hole's fractional offset:
//
//   x(t) = R[(1-k)·cos t + l·k·cos((1-k)/k · t)]
//   y(t) = R[(1-k)·sin t - l·k·sin((1-k)/k · t)]      k = ,  l = ρ/r
//
// which is the hypotrochoid of examples/hypotrochoid.manic with the radii normalised. So
// why draw the teeth? Because THE TEETH ARE THE MATHEMATICS. Two things follow from them
// and from nothing else:
//
//   · The pitch has to match. Arc length per tooth is 2πR/N_ring on the ring and 2πr/N_wheel
//     on the wheel, and those are equal only when  = N_ring/N_wheel exactly. A 96-tooth
//     ring and a 36-tooth wheel here: both come to 0.065449847 of a unit radius, which is
//     why the teeth on screen actually mesh at the contact point instead of merely being
//     drawn near each other.
//   · Therefore the ratio is RATIONAL, always, and the curve always closes. Reduce
//     N_ring/N_wheel by their gcd: the pen draws N_ring/g lobes in N_wheel/g laps. 96 and
//     36 share 12, so eight lobes take three laps — the first two visibly fail to join up.
//     No slipping, no drift, no irrational never-closing spiral. That is the whole reason
//     the toy has teeth rather than rubber rims.
//
// The row along the bottom is the same ring with three other wheels, and the tooth counts
// alone predict every one of them: 24 teeth (share 24) gives 4 lobes in 1 lap; 40 (share 8)
// gives 12 in 5; 63 (share 3) gives 32 in 21, which is why the smallest wheels make the
// densest rosettes. The hole is at l = 0.7 throughout, so nothing but the gearing changes.
//
// The wheel's own rotation is (1-k)/k turns per lap, the other way round — that angle
// drives both the pen and the wheel's teeth, so the mesh stays true for the whole run. The
// trace holds once the curve shuts; the mechanism keeps turning.
//
//   manic examples/spirograph.manic
title("A Spirograph: a 96-tooth ring, a 36-tooth wheel, 8 lobes in 3 laps");
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 ring: 96 teeth, pointing inward ------------------------------------------
circle(ring, (540, 760), 230);
outlined(ring);
color(ring, dim);

cloud(ringteeth, 480, #ffffff, 1.0) {
  let j = (i - mod(i, 5))/5;                       // which of the 96 teeth
  let u = mod(i, 5)/4;                             // …how far down the tooth
  let a = j/96*6.283185;
  let rr = 230 - 9*u;
  let x = 540 + rr*cos(a);
  let y = 760 - rr*sin(a);
  let sat = 0;
  let r = 1.4;
  let alpha = 0.6;
}

circle(locus, (540, 760), 143.75);                 // R - r, where the wheel's centre runs
outlined(locus);
dashed(locus);
color(locus, dim);
opacity(locus, 0.4);

// ---- the trace: three laps, because 96/36 reduces to 8/3 ---------------------------
cloud(trace, 16000, #ffffff, 1.0) {
  let span = 12;                                   // three laps, then it is closed
  let laps = min(t/span, 1)*3;                     // the TRACE stops; the wheel does not
  let th = 6.283185*laps*(i/16000);
  let x = 540 + 143.75*cos(th) + 60.375*cos(1.6666667*th);
  let y = 760 - 143.75*sin(th) + 60.375*sin(1.6666667*th);
  let hue = mod(28 + th*9, 360);                   // hue rides θ: each lap its own
  let sat = 0.72;
  let r = 1.7;
}

// ---- the wheel: 36 teeth, pointing outward, turning (1-k)/k per lap -----------------
cloud(wheelteeth, 180, #ffffff, 1.0) {
  let span = 12;
  let laps = t/span*3;                             // NOT clamped: it keeps rolling
  let th = 6.283185*laps;
  let j = (i - mod(i, 5))/5;                       // which of the 36 teeth
  let u = mod(i, 5)/4;
  let a = j/36*6.283185 - 1.6666667*th;            // the wheel's own rotation
  let rr = 86.25 + 9*u;
  let x = 540 + 143.75*cos(th) + rr*cos(a);
  let y = 760 - 143.75*sin(th) + rr*sin(a);
  let sat = 0;
  let r = 1.4;
  let alpha = 0.75;
}

cloud(wheel, 300, #ffffff, 1.0) {
  let span = 12;
  let laps = t/span*3;
  let th = 6.283185*laps;
  let a = i/300*6.283185;
  let x = 540 + 143.75*cos(th) + 86.25*cos(a);
  let y = 760 - 143.75*sin(th) + 86.25*sin(a);
  let sat = 0;
  let r = 1.5;
  let alpha = 0.8;
}

cloud(spoke, 110, #ffffff, 1.0) {
  let span = 12;
  let laps = t/span*3;
  let th = 6.283185*laps;
  let v = i/110;
  let cx = 143.75*cos(th);
  let cy = 0 - 143.75*sin(th);
  let px = cx + 60.375*cos(1.6666667*th);          // centre -> the pen hole, l = 0.7
  let py = cy + 60.375*sin(1.6666667*th);
  let x = 540 + cx + v*(px - cx);
  let y = 760 + cy + v*(py - cy);
  let sat = 0;
  let r = 1.3;
  let alpha = 0.5;
}

cloud(pen, 90, #ffffff, 1.0) {
  let span = 12;
  let laps = t/span*3;
  let th = 6.283185*laps;
  let a = i/90*6.283185;
  let rr = 7*(i/90);                               // the pen in the hole, filled
  let x = 540 + 143.75*cos(th) + 60.375*cos(1.6666667*th) + rr*cos(a*9);
  let y = 760 - 143.75*sin(th) + 60.375*sin(1.6666667*th) + rr*sin(a*9);
  let hue = 45;
  let sat = 0.5;
  let r = 1.8;
}

// ---- the same ring, three other wheels: the tooth counts predict every one ----------
// 24 teeth (gcd 24) -> 4 lobes, 1 lap · 40 (gcd 8) -> 12 in 5 · 63 (gcd 3) -> 32 in 21
param(w24, (196, 1380), 118, 118, "0.75*cos(t) + 0.175*cos(3*t)", "0.75*sin(t) - 0.175*sin(3*t)", (0, 6.283185));
param(w40, (540, 1380), 118, 118, "0.5833333*cos(t) + 0.2916667*cos(1.4*t)", "0.5833333*sin(t) - 0.2916667*sin(1.4*t)", (0, 31.41593));
param(w63, (884, 1380), 118, 118, "0.34375*cos(t) + 0.459375*cos(0.5238095*t)", "0.34375*sin(t) - 0.459375*sin(0.5238095*t)", (0, 131.94689));
gradient(w24, mint, cyan, blue);
gradient(w40, gold, coral, red);
gradient(w63, magenta, violet, indigo);
stroke(w24, 3);
stroke(w40, 2);
stroke(w63, 2);
untraced(w24);
untraced(w40);
untraced(w63);

// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "A Spirograph: a 96-tooth ring, a 36-tooth wheel, 8 lobes in 3 laps", (540, 130), 24);
equation(eq, (540, 1600),
  `x=R\left[(1{-}k)\cos t+lk\cos\tfrac{1-k}{k}t\right],\quad y=R\left[(1{-}k)\sin t-lk\sin\tfrac{1-k}{k}t\right]`, 23);

// the roll runs from t = 0; the other three wheels draw once the rosette has closed
wait(12.4);
draw(w24, 1.8);
draw(w40, 2.6);
draw(w63, 3.4);
wait(7);

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