Generative & recursive
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
spiral-families
The six spirals nature keeps reusing, side by side, with no narration at all — every panel is
one closed-form formula and thousands of points of light, and the plate explains itself.
FIBONACCI r = aphi^(2t/pi) (nautilus, galaxies), VOGEL 137.5 degrees per seed (sunflowers,
pinecones), ARCHIMEDEAN r = a + bt (watch springs), FERMAT r = asqrt(t) with BOTH arms
(lens design), LOGARITHMIC r = ae^(bt) with three arms (hurricane rainbands), and the real
CURLICUE - the running sum of unit steps each turned by piphi*m^2, which a cloud can never
do (its formulas are pure in (i,t) and cannot accumulate), so it is computed exactly by
build-time sum reductions over the loop index and drawn as 360 real segments. Physics
respected: the log spirals sample uniformly in RADIUS, since their arc length grows with
radius, and each panel unfurls from its centre because opacity is saturate((t-start)*k - i/N)
- arithmetic, not keyframes. The background is the same law as wallpaper: level sets of
(angle - ln r / b) ARE logarithmic spirals. Then the UZUMAKI finale: one bound
parameterdraws all six families off their panels into a single CHAOTIC maelstrom - every point riding its own pitch, arm and phase from fract(sin(i)) hashes, with noise kneading the radius - while a torn-spiralshadervortex reads the same number and rises with them.
// spiral-families — the six spirals nature keeps reusing, side by side, each one a single
// closed-form formula and about five thousand points of light.
//
// Fibonacci r = a·φ^(2θ/π) nautilus shells, galaxies
// Vogel θ = n · 137.5° sunflower seeds, pinecones
// Archimedean r = a + bθ watch springs, coiled rope
// Fermat r = a·√θ optical lenses (both arms)
// Logarithmic r = a·e^(bθ) hurricanes (three arms)
// Curlicue φ = 2πφ·n² fractal art
//
// Every panel is one `cloud`: position, size and colour are closed-form functions of the
// point index `i` and live time `t`, so each spiral genuinely turns yet the whole plate stays
// a pure function of `t` — it scrubs and records exactly. The unfurl is not a keyframe
// either: each point's opacity is `saturate((t − start)·rate − i/N)`, so the light travels
// out from the centre because of arithmetic, not animation.
//
// Two honest notes. A LOGARITHMIC spiral has arc length proportional to radius, so the
// Fibonacci and hurricane panels sample uniformly in RADIUS — that is what makes their
// windings even instead of piling up at the rim. And the curlicue here is the quadratic-angle
// form: a cloud formula is pure in `(i, t)`, so it cannot accumulate the running sum of unit
// steps the classical curlicue is built from.
//
// manic examples/spiral-families.manic
title("Six Spirals Nature Keeps Reusing — manic");
canvas("16:9");
template("black");
bloom(0.38, 0.46, 26);
// the mark, above everything, for the whole film
text(brand, (640, 28), "maniclang.com");
display(brand); size(brand, 19); color(brand, cyan); opacity(brand, 0.8); plate(brand, 0.5); z(brand, 100);
text(ttl, (640, 70), "Six spirals nature keeps reusing");
display(ttl); size(ttl, 30); bold(ttl); color(ttl, fg); hidden(ttl);
// A background that obeys the same law the panels do: the level sets of (angle − ln r / b)
// ARE logarithmic spirals, so this is one giant log spiral used as wallpaper. Its eye sits
// below the frame, so the plate gets broad sweeping arms instead of a bullseye behind the
// grid, and the very top stays clean where the mark and the title live. Kept in a 0.02–0.10
// brightness band on purpose: it has to elevate the six spirals, never compete with them.
shader(bg) {
let x = (u - 0.5)*asp*1.25;
let y = v + 0.62;
let rr = length(x, y) + 0.02;
let a = atan2(y, x);
let ph = a - log(rr)/0.42;
let arms = 0.5 + 0.5*sin(2.0*ph + t*0.16);
let fine = 0.5 + 0.5*sin(5.0*ph - t*0.09);
let swirl = 0.68*arms + 0.32*fine;
let grain = 0.5 + 0.5*fbm(x*3.4 + t*0.02, y*3.4);
let top = smoothstep(0.0, 0.3, v);
let hue = 238 - 34.0*swirl;
let sat = 0.76 - 0.22*swirl;
let val = 0.016 + 0.078*swirl*top + 0.013*grain*top;
}
z(bg, -10);
// UZUMAKI — how far the whole plate has been drawn into a single spiral. Every panel's cloud
// reads this parameter BY NAME, so the finale is not six separate animations: it is one number,
// and each swarm swirls toward the centre because its own formula says so.
parameter(pull, (150, 690), 0, 0, 1, "uzumaki", 2); hidden(pull.widget);
shader(vortex) {
let x = (u - 0.5)*asp;
let y = v - 0.5;
let rr = length(x, y) + 0.02;
let a = atan2(y, x);
// a violent domain warp: the ANGLE itself is kneaded by noise, so the arms tear as they turn
let w = 0.6*snoise(x*3.2 + t*0.15, y*3.2 - t*0.1);
let ph = a + w - log(rr)/0.17;
let arms = 0.5 + 0.5*sin(4.0*ph + t*1.1);
let core = gaussian(rr, 0.17);
let edge = saturate(1.25 - rr*1.15);
let hue = 292 - 46.0*arms + 34.0*core;
let sat = 0.86 - 0.34*core;
let val = (0.05 + 0.52*arms*arms + 0.55*core)*edge;
let alpha = pull*saturate(0.12 + 1.15*arms*arms + core)*edge;
}
z(vortex, -5);
// ============================== panel furniture ==============================
// three columns, two rows: names above each spiral, its formula under the name, and what
// grows that way underneath the light
text(n1, (235, 116), "Fibonacci"); text(n2, (640, 116), "Vogel");
text(n3, (1045, 116), "Archimedean"); text(n4, (235, 398), "Fermat");
text(n5, (640, 398), "Logarithmic"); text(n6, (1045, 398), "Curlicue");
display(n1); display(n2); display(n3); display(n4); display(n5); display(n6);
size(n1, 22); size(n2, 22); size(n3, 22); size(n4, 22); size(n5, 22); size(n6, 22);
bold(n1); bold(n2); bold(n3); bold(n4); bold(n5); bold(n6);
hue(n1, 45); hue(n2, 92); hue(n3, 190); hue(n4, 215); hue(n5, 320); hue(n6, 272);
hidden(n1); hidden(n2); hidden(n3); hidden(n4); hidden(n5); hidden(n6);
equation(f1, (235, 150), `r = a\,\varphi^{2\theta/\pi}`, 21);
equation(f2, (640, 150), `\theta_n = n \cdot 137.5^{\circ}`, 21);
equation(f3, (1045, 150), `r = a + b\,\theta`, 21);
equation(f4, (235, 432), `r = a\sqrt{\theta}`, 21);
equation(f5, (640, 432), `r = a\,e^{b\theta}`, 21);
equation(f6, (1045, 440), `z_n = \sum_{m<n} e^{i\pi\varphi m^2}`, 16);
hue(f1, 45); hue(f2, 92); hue(f3, 190); hue(f4, 215); hue(f5, 320); hue(f6, 272);
hidden(f1); hidden(f2); hidden(f3); hidden(f4); hidden(f5); hidden(f6);
text(w1, (235, 366), "nautilus shells · galaxies");
text(w2, (640, 366), "sunflower seeds · pinecones");
text(w3, (1045, 366), "watch springs · coiled rope");
text(w4, (235, 648), "optical lenses");
text(w5, (640, 648), "hurricanes");
text(w6, (1045, 648), "fractal art");
display(w1); display(w2); display(w3); display(w4); display(w5); display(w6);
size(w1, 17); size(w2, 17); size(w3, 17); size(w4, 17); size(w5, 17); size(w6, 17);
color(w1, dim); color(w2, dim); color(w3, dim);
color(w4, dim); color(w5, dim); color(w6, dim);
hidden(w1); hidden(w2); hidden(w3); hidden(w4); hidden(w5); hidden(w6);
// ============================== 1 · FIBONACCI ==============================
// the golden spiral: every quarter turn multiplies the radius by φ = 1.618…, which is a
// logarithmic spiral with b = ln(φ)/(π/2) = 0.3063. Sampled uniformly in RADIUS, because a
// log spiral's arc length grows with its radius.
cloud(s1, 5200, gold, 0.85) {
let u = i/5200;
let rr = 1.2 + 76*u;
let th = log(rr/0.04)/0.3063 + 0.16*t;
let px = 235 + rr*cos(th);
let py = 258 - rr*sin(th);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 0.9 + 1.5*u;
let hue = 38 + 26*u;
let sat = 0.85;
let val = 0.72 + 0.28*u;
let alpha = saturate((t - 1.0)*2.4 - u*1.9);
}
glow(s1, 2);
// ============================== 2 · VOGEL ==============================
// phyllotaxis: seed n at 137.5° from the last and √n out. No two seeds crowd, which is why
// sunflowers, pinecones and pineapples all settle on this one.
cloud(s2, 1500, lime, 0.9) {
let n = i + 1;
let u = i/1500;
let rr = 78*sqrt(n/1500);
let th = n*2.39996 + 0.16*t;
let px = 640 + rr*cos(th);
let py = 258 - rr*sin(th);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 1.3 + 1.4*u;
let hue = 76 + 40*u;
let sat = 0.8;
let val = 0.7 + 0.3*u;
let alpha = saturate((t - 2.0)*2.4 - u*1.9);
}
glow(s2, 2);
// ============================== 3 · ARCHIMEDEAN ==============================
// equal spacing every turn — the coil of a watch spring or a rope on a deck. Sampled
// uniformly in θ, since that IS the defining regularity.
cloud(s3, 5200, cyan, 0.85) {
let u = i/5200;
let th = u*37.7;
let rr = 3.5 + 1.98*th;
let px = 1045 + rr*cos(th + 0.16*t);
let py = 258 - rr*sin(th + 0.16*t);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 1.0 + 1.1*u;
let hue = 184 + 24*u;
let sat = 0.8;
let val = 0.72 + 0.28*u;
let alpha = saturate((t - 3.0)*2.4 - u*1.9);
}
glow(s3, 2);
// ============================== 4 · FERMAT ==============================
// r = a√θ, and the real thing has BOTH arms — `mod(i,2)` picks one, so the panel shows the
// full双 curve. Equal AREA per turn, which is why lens and mirror designers use it.
cloud(s4, 5200, cyan, 0.85) {
let u = i/5200;
let arm = mod(i, 2)*pi;
let th = u*30;
let rr = 14.2*sqrt(th);
let px = 235 + rr*cos(th + arm + 0.16*t);
let py = 540 - rr*sin(th + arm + 0.16*t);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 1.0 + 1.0*u;
let hue = 206 + 26*u;
let sat = 0.82;
let val = 0.7 + 0.3*u;
let alpha = saturate((t - 4.0)*2.4 - u*1.9);
}
glow(s4, 2);
// ============================== 5 · LOGARITHMIC ==============================
// the same law as Fibonacci with a fatter pitch, and three arms — a hurricane's rainbands.
// Again sampled uniformly in radius; the bright core is the eye.
cloud(s5, 5400, magenta, 0.85) {
let u = i/5400;
let arm = mod(i, 3)*2.0944;
let rr = 1.0 + 77*u;
let th = log(rr/1.6)/0.30 + arm + 0.34*t;
let px = 640 + rr*cos(th);
let py = 540 - rr*sin(th);
let dx = px - 640;
let dy = py - 360;
let dd = hypot(dx, dy)*(1 - 0.30*pull);
let aa = atan2(dy, dx) + pull*2.6;
let sx = 640 + dd*cos(aa);
let sy = 360 + dd*sin(aa);
// The destination is a CHAOTIC spiral, not a tidy one. Two hashes give every point its own
// pitch, its own arm and its own phase, and drifting noise kneads the radius — so the six
// families do not line up into one clean curve, they collapse into a maelstrom that is
// still, everywhere, logarithmic. Deterministic chaos: no rand(), just fract(sin(i)).
let h1 = fract(sin(i*12.9898)*43758.545);
let h2 = fract(sin(i*78.233)*12345.678);
let arm = floor(h2*5)*1.2566;
let pitch = 0.20 + 0.26*h1;
let trr = 10 + 244*u + 34*snoise(u*7.0 + h2*9.0, t*0.25);
let tth = log(max(trr, 8)/0.05)/pitch + arm + 0.5*t + 2.4*h1;
let x = (1 - pull)*sx + pull*(640 + trr*cos(tth));
let y = (1 - pull)*sy + pull*(360 - trr*sin(tth));
let r = 0.9 + 1.4*u;
let hue = 300 + 40*u;
let sat = 0.78;
let val = 0.95 - 0.3*u;
let alpha = saturate((t - 5.0)*2.4 - u*1.9);
}
glow(s5, 2);
// ============================== 6 · CURLICUE ==============================
// The REAL curlicue, not a stand-in: z_n is the running sum of unit steps, each turned by
// π·s·m². A `cloud` cannot do this — its formulas are pure in (i, t) and cannot accumulate —
// but a build-time `sum` reduction over the loop index computes the exact partial sum, so the
// path is drawn as 360 real segments. The golden fraction makes the classic branching,
// self-similar clusters; nothing here is random and nothing is recursive.
for n in 0..360 {
line(s6{n},
(975 + 6.5*sum(m in 0..n : cos(pi*0.618034*m*m)),
566 - 6.5*sum(m in 0..n : sin(pi*0.618034*m*m))),
(975 + 6.5*sum(m in 0..n+1 : cos(pi*0.618034*m*m)),
566 - 6.5*sum(m in 0..n+1 : sin(pi*0.618034*m*m))));
hue(s6{n}, 258 + n/11);
untraced(s6{n});
tag(s6{n}, s6);
}
glow(s6, 2);
// ---- the uzumaki finale ----
svg(maki1, (250, 366), "asset:svg/emoji/1f365.svg", 74); hidden(maki1);
svg(maki2, (1030, 366), "asset:svg/emoji/1f365.svg", 74); hidden(maki2);
text(uzulab, (640, 648), "UZUMAKI");
display(uzulab); size(uzulab, 38); bold(uzulab); color(uzulab, fg); plate(uzulab, 0.62); z(uzulab, 50); hidden(uzulab);
// ================================= the film =================================
show(ttl, 1.0);
wait(0.5);
// each panel introduces itself as its own light arrives — the name, the formula and what grows
// that way are already on screen, so the film does not narrate them
stagger(1.0) {
par { show(n1, 0.5); show(f1, 0.5); show(w1, 0.4); }
par { show(n2, 0.5); show(f2, 0.5); show(w2, 0.4); }
par { show(n3, 0.5); show(f3, 0.5); show(w3, 0.4); }
par { show(n4, 0.5); show(f4, 0.5); show(w4, 0.4); }
par { show(n5, 0.5); show(f5, 0.5); show(w5, 0.4); }
par { show(n6, 0.5); show(f6, 0.5); show(w6, 0.4); }
}
draw(s6, 2.4, smooth);
wait(1.0);
// they all turn, so the dwell is not dead time
wait(4.0);
wait(3.6);
// ============================== UZUMAKI ==============================
par {
fade(n1, 0.7); fade(n2, 0.7); fade(n3, 0.7); fade(n4, 0.7); fade(n5, 0.7); fade(n6, 0.7);
fade(f1, 0.7); fade(f2, 0.7); fade(f3, 0.7); fade(f4, 0.7); fade(f5, 0.7); fade(f6, 0.7);
fade(w1, 0.6); fade(w2, 0.6); fade(w3, 0.6); fade(w4, 0.6); fade(w5, 0.6); fade(w6, 0.6);
fade(ttl, 0.8);
}
wait(1.4);
// one number does all of this: each swarm reads `pull` and swirls in on its own account,
// and the curlicue path swings round with them
par {
to(pull, value, 1, 4.6, smooth);
turn(s6, (640, 360), 80, 4.6, smooth);
to(s6, opacity, 0.2, 4.6, smooth);
}
wait(1.8);
// the merged spiral gets a beat on its own, then steps back so the word can sit on it
par {
to(s1, opacity, 0.17, 1.0); to(s2, opacity, 0.17, 1.0); to(s3, opacity, 0.17, 1.0);
to(s4, opacity, 0.17, 1.0); to(s5, opacity, 0.17, 1.0);
}
par { show(maki1, 0.7); show(maki2, 0.7); }
show(uzulab, 0.9);
wait(2.8);
// ================================= endcard =================================
par {
fade(maki1, 0.6); fade(maki2, 0.6);
fade(uzulab, 0.7);
to(pull, value, 0.42, 1.6, smooth);
}
wait(2.8);
art-golden-angle
The golden angle as glowing particle art: 1600 seeds bloom from the centre (angle = i·137.5°, radius = √i) into a sunflower. Nudge the divergence a fraction off φ and spiral voids tear open — only 137.5° packs the head seamlessly. Pure cloud, additive glow.
// The Golden Angle — why sunflowers spiral. 1600 seeds, each placed one turn of
// 137.5° from the last (φ's angle), at radius √i. That single angle packs the
// plane with no gaps and no seam — nudge it a fraction and spiral voids tear open.
// Pure formula-driven `cloud`: angle = i·div, radius = √i.
//
// manic examples/art-golden-angle.manic
title("The Golden Angle — 137.5°");
canvas("16:9");
template("black");
text(hdr, (640, 74), "The Golden Angle — Manic", 32);
cloud(seeds, 1600) {
let g = 137.507; // φ's angle: 360·(1 − 1/φ) degrees
// between t≈5 and t≈8 the divergence dips 0.7° off golden — watch gaps open
let bump = 0.25 * (1 + tanh((t - 5.0) * 2.4)) * (1 + tanh((8.0 - t) * 2.4));
let div = g - bump * 0.7;
let ang = i * div * 0.0174533; // degrees → radians
let rad = 7.3 * sqrt(i + 0.5); // √i spacing → uniform density
let x = 640 + rad * cos(ang);
let y = 392 + rad * sin(ang);
// bloom: seeds appear from the centre outward over the first ~3 s
let born = i / 1600;
let alpha = 0.5 * (1 + tanh((t - born * 3.0 - 0.4) * 4));
let hue = mod(48 - rad * 0.14, 360); // gold core → magenta rim
let sat = 0.9;
let val = 0.62; // <1 shows hue; glow re-brightens
let rnd = mod(sin(i * 17.1) * 43758.5453, 1);
let r = 2.9 + 1.4 * rnd; // round discs (>2.5px), size grain
}
// additive glow: overlapping seeds bloom into light — a lit sunflower head
glow(seeds, 4);
text(cap, (640, 700), "1600 seeds, each turned 137.5° from the last.", 22);
hidden(cap);
wait(0.8);
show(cap);
wait(2.6);
say(cap, "The golden angle — φ's turn. Perfect packing, no seam.");
wait(2.0);
say(cap, "A fraction off, and spiral voids tear open…");
wait(2.8);
say(cap, "…only 137.5° fills the head without a gap.");
wait(2.2);
art-circle-area-proof
Area = πr² as a SWARM: the same particles fill a disc, then flow into a parallelogram of the same area whose scalloped wedge-edges refine and flatten toward a rectangle (the limit). Nothing is added or removed — the conserved count IS the proof.
// Area = πr², as a SWARM — and the LIMIT that finishes the proof.
// The same particles fill a disc, flow into a lumpy wedge-strip, then the humps
// MULTIPLY and FLATTEN (4 → 8 → 16 wedges …) until the edge is straight: a
// πr × r rectangle. Nothing is added or removed — the count is the area (πr²),
// conserved the whole way. One `cloud`, all formula-driven.
//
// manic examples/art-circle-area-proof.manic
title("Area of a circle = πr²");
canvas("16:9");
template("black");
// on-screen heading, top-centre, held throughout
text(hdr, (640, 74), "Circle Area of Proof — Manic", 32);
cloud(swarm, 3200) {
// ---- uniform grid index → (fx, fy) in the unit square ---------------------
let cols = 80;
let ci = mod(i, cols);
let ri = (i - ci) / cols; // integer row 0..39
let fx = ci / 79; // 0..1 across the width
let fy = ri / 39; // 0..1 top → bottom
// a little hash jitter so the grid reads as a filled field, not a lattice
let jx = (mod(sin(i * 12.9898) * 43758.5453, 1) - 0.5) * 7;
let jy = (mod(sin(i * 78.2330) * 43758.5453, 1) - 0.5) * 7;
// ---- destination: a parallelogram with SCALLOPED (wedge) edges ------------
let wdt = 565; let hlf = 90; // base πr ≈ 565, height r = 180
let x0 = 313; let yc = 340; let slnt = 90;
// refinement s: 0 (few coarse wedges) → 1 (many fine wedges → rectangle)
let s = 0.5 * (1 + tanh((t - 5.6) * 0.7));
let nh = 2 + 6 * s; // humps per edge: 2 → 8
let amp = 48 * (1 - s) + 2; // hump depth: 50 → 2 (flattens)
let wv = amp * cos(6.2831853 * nh * fx);
let topE = yc - hlf - wv; // top edge bulges up at the humps
let botE = yc + hlf + wv; // bottom edge bulges down
let sx = x0 + fx * wdt + (1 - fy) * slnt + jx;
let sy = topE + fy * (botE - topE) + jy;
// ---- start: a uniform disc of the SAME area (golden-angle sunflower) ------
let gr = sqrt((i + 0.5) / 3200);
let ang = i * 2.399963;
let dx = 640 + 180 * gr * cos(ang);
let dy = 340 + 180 * gr * sin(ang);
// ---- blend disc → strip, then the strip refines to a rectangle ------------
let b = 0.5 * (1 + tanh((t - 3.2) * 1.1));
let x = dx * (1 - b) + sx * b;
let y = dy * (1 - b) + sy * b;
let hue = mod(330 - gr * 140, 360); // Manic neon: magenta core → cyan rim
let sat = 0.9;
let val = 0.6; // <1 shows the hue; additive glow re-brightens overlaps
// varied radius > 2.5px → true ROUND discs (≤2.5px render as squares), with size grain
let rnd = mod(sin(i * 91.7) * 43758.5453, 1);
let r = 2.8 + 2.2 * rnd;
}
// additive glow: dense/overlapping points accumulate into light — soft nebula cores
glow(swarm, 4);
text(cap, (640, 630), "π r² particles — a disc's worth.", 24);
hidden(cap);
wait(0.6);
show(cap);
wait(1.8);
say(cap, "Cut into wedges and re-lay them — a lumpy strip.");
wait(2.2);
say(cap, "More wedges, finer and finer — the humps flatten…");
wait(2.6);
say(cap, "…in the limit, a πr × r rectangle. Area = π r².");
wait(2.4);
art-calculus-sine
Sine, its derivative, its Riemann area and a riding tangent — five index-partitioned particle families in one cloud, cross-faded in beats. Thick glowing wave-ribbons with bright cores over a particle coordinate-grid: the swarm is the subject, not a plotted line.
// Sine, its derivative, its Riemann area & a riding tangent — all PARTICLE ART.
// 12000 dots in five families, one formula, no plot/coords/riemann built-ins:
// 0 SINE ribbon (cyan) — a thick glowing wave-swarm
// 1 COSINE ribbon (gold) — the derivative, cos x = the slope
// 2 coordinate GRID (faint) — the x/y plane in dots
// 3 RIEMANN columns (magenta)— particles fill the strips under the wave
// 4 riding TANGENT swarm (white) — a line that tilts to cos x as it sweeps
// The families CROSS-FADE in beats so each idea reads on its own, then a finale.
//
// manic examples/art-calculus-sine.manic
title("Sine · derivative · area");
canvas("16:9");
template("black");
text(hdr, (640, 70), "Sine · its Derivative · its Area — Manic", 30);
cloud(field, 12000) {
let g = floor(i / 2400); // family 0..4
let m0 = clamp(1 - max(g, -g), 0, 1);
let m1 = clamp(1 - max(g-1, 1-g), 0, 1);
let m2 = clamp(1 - max(g-2, 2-g), 0, 1);
let m3 = clamp(1 - max(g-3, 3-g), 0, 1);
let m4 = clamp(1 - max(g-4, 4-g), 0, 1);
let li = mod(i, 2400);
let loc = li / 2399;
let ox = 640; let oy = 384;
let sx = 92; let sy = 118;
let mx = (loc * 2 - 1) * 3.14159; // math x ∈ [-π, π]
let ph = mx + t * 0.9; // the wave travels (gentle)
let sp = rand(i) + rand(i + 4051) - 1; // -1..1, dense near 0
let asp = max(sp, -sp);
// 0/1 — thick sine & cosine ribbons
let wvX = ox + mx * sx;
let sinY = oy - (sin(ph) + sp * 0.24) * sy;
let cosY = oy - (cos(ph) + sp * 0.24) * sy;
// 2 — faint particle grid
let gridX = ox + (mod(li, 52) / 51 * 2 - 1) * 320;
let gridY = oy - (floor(li / 52) / 51 * 2 - 1) * 178;
// 3 — Riemann columns: 16 strips, particles fill axis → sin height (signed)
let bi = floor(loc * 16);
let barMX = 0.0 - 3.14159 + (bi + 0.5) / 16 * 6.28318;
let barH = sin(barMX + t * 0.9);
let rmX = ox + barMX * sx + (rand(i + 11) - 0.5) * (6.28318 / 16 * sx * 0.78);
let rmY = oy - rand(i + 23) * barH * sy;
// 4 — a tangent line that sweeps and tilts to the slope cos(x0)
let x0 = 0.0 - 2.3 + mod(t * 0.4, 1) * 4.6;
let ss = (loc * 2 - 1) * 0.9;
let tanH = sin(x0 + t * 0.9) + cos(x0 + t * 0.9) * ss; // value + slope·offset
let tgX = ox + (x0 + ss) * sx;
let tgY = oy - tanH * sy + (rand(i + 77) - 0.5) * 8;
let x = (m0 + m1) * wvX + m2 * gridX + m3 * rmX + m4 * tgX;
let y = m0 * sinY + m1 * cosY + m2 * gridY + m3 * rmY + m4 * tgY;
// ---- beats: each idea rises, then clears for the next ----------------------
let rmA = clamp((t - 4.5) * 1.0, 0, 1) * clamp((10.0 - t) * 1.0, 0, 1); // area 4.5–10
let tgA = clamp((t - 9.5) * 1.0, 0, 1); // tangent 9.5→end
let cosDim = 1 - 0.55 * rmA; // derivative steps back while area shows
let hue = m0 * 192 + m1 * 46 + m2 * 210 + m3 * 328 + m4 * 50;
let sat = m0 * 1.0 + m1 * 1.0 + m2 * 0.3 + m3 * 0.8 + m4 * 0.3;
let core = 0.5 * (1 - 0.7 * asp);
let val = m0 * core + m1 * core + m2 * 0.14 + m3 * 0.32 + m4 * 0.75;
let alpha = m0 * 1.0 + m1 * cosDim + m2 * 0.45 + m3 * rmA * 0.8 + m4 * tgA;
let r = m2 * 1.8 + (m0 + m1) * (2.4 + rand(i + 88))
+ m3 * (2.2 + rand(i + 5)) + m4 * (2.7 + rand(i + 9));
}
glow(field, 2);
text(cap, (640, 700), "cyan sin x · gold cos x — its slope.", 22);
hidden(cap);
wait(1.0);
show(cap);
wait(3.4);
say(cap, "Riemann strips — the area under the wave, in dots.");
wait(5.0);
say(cap, "The sum clears; a tangent rides — its tilt IS cos x.");
wait(4.0);
say(cap, "A function, its slope, its area — one swarm.");
wait(3.2);
exponential-shells
A living de Sitter volume — ∭ a(t) ∝ e^{Ht} rendered as ~320,000 points of light. A grainy
multicolour spherical CAP meets a family of exponentially-growing spherical SHELLS tangent at a
shared hot origin — one cloud3 batch per colour family, every point a pure closed-form f(i). A
dense gold junction glows additively (glow) into the white-hot core; bloom() gives the milky
cosmic light. A full 25s camera3 orbit turns the face-on concentric rings into the offset 3-D
shells and loops cleanly back. Showcases cloud3 at scale + per-point sat/val, additive glow,
and the bloom post-process together.
// engine-test-13 — native 3D reconstruction of the reference.
//
// The picture is not a flat disc. It is a large, grainy spherical particle cap
// meeting a family of exponentially growing particle shells at a shared hot
// origin. Looking down their common axis makes concentric rings; a full camera
// orbit reveals the offset spherical shells and returns to the opening frame.
title("∭ 𝘢(𝘵) ∝ eᴴᵗ");
canvas(1638, 1482);
template("black");
bloom(0.90, 0.22, 52);
// Keep the mathematical title fixed in screen space while the 3-D field turns.
text(formulaTitle, (819, 70), "∭ 𝘢(𝘵) ∝ eᴴᵗ");
size(formulaTitle, 44);
color(formulaTitle, gold);
bold(formulaTitle);
display(formulaTitle);
sticky(formulaTitle);
z(formulaTitle, 20);
// The camera begins on the cap side of the common tangent. It keeps turning in
// one direction throughout the 20-second hold, completing 1.5 revolutions.
// That puts opposite face-on views about 6.67 s apart, matching the reference
// cadence; four fast turns made the alternating side views read as oscillation.
// The wider field of view keeps the luminous rim inside the complete orbit.
camera3((-32, 0, 0), (0, 0, 0), 21.0, perspective);
// --- large spherical cap --------------------------------------------------
// Several low-opacity random skins give the reference its fine, multicolour
// grain. Surface-point foreshortening naturally creates the bright rim.
cloud3(outerRose, 52000, #d78676, 0.095) {
let ct = -1 + 1.18 * rand2(i, 10.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 11.9);
let rr = 4.66 + 0.075 * (rand2(i, 11.3) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.42 + 0.58 * (-ct);
let r = 0.042;
}
glow(outerRose, 1);
cloud3(outerViolet, 48000, #72589f, 0.072) {
let ct = -1 + 1.18 * rand2(i, 22.7);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 24.3);
let rr = 4.69 + 0.09 * (rand2(i, 23.9) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.30 + 0.70 * (1 + ct);
let r = 0.038;
}
glow(outerViolet, 1);
cloud3(outerSilver, 36000, #b8d8ef, 0.072) {
let ct = -1 + 1.18 * rand2(i, 36.3);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 38.7);
let rr = 4.72 + 0.055 * (rand2(i, 37.1) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.22 + 0.78 * (1 + ct);
let r = 0.034;
}
glow(outerSilver, 1);
// A sparse warm skin just outside the main boundary produces the thin amber
// fringe visible around the lavender rim in the reference.
cloud3(outerAmber, 18000, #d67425, 0.026) {
let ct = -1 + 1.18 * rand2(i, 50.3);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 52.9);
let rr = 4.79 + 0.08 * (rand2(i, 51.7) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.35 + 0.65 * (-ct);
let r = 0.036;
}
glow(outerAmber, 1);
// A broad, extremely faint splat layer closes the gaps between the fine
// grains. Additive accumulation turns it into the milky cosmic illumination
// visible in the recording without replacing the surface texture.
cloud3(outerCosmos, 90000, #b99bbd, 0.012) {
let ct = -1 + 1.18 * rand2(i, 118.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 121.7);
let rr = 4.69 + 0.10 * (rand2(i, 119.9) - 0.5);
let x = rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let alpha = 0.32 + 0.68 * (-ct);
let r = 0.085;
}
glow(outerCosmos, 1);
// --- exponential shell family -------------------------------------------
// Every sphere is tangent at the origin. The gold family grows inward with
// centre=(-radius,0,0); pink/violet/cyan grow outward from (+radius,0,0).
// Exponential radius growth turns the face-on rings into the nested horn seen
// edge-on. `s` selects one sphere and `j` selects a deterministic surface point;
// each colour family remains one efficient renderer batch.
cloud3(shellGold, 33600, #ffad24, 0.080) {
let per = 4200;
let s = floor(i / per);
let j = i - s * per;
let sr = 0.080 * exp(0.310 * s);
let ct = 1 - 2 * rand2(j, s + 63.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(j, s + 65.7);
let rr = sr + 0.012 * (rand2(i, s + 4.2) - 0.5);
let x = -sr + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.018 + 0.0012 * s;
}
glow(shellGold, 1);
cloud3(shellPink, 9200, #ff79c6, 0.075) {
let per = 4600;
let s = floor(i / per);
let j = i - s * per;
let k = s + 8;
let sr = 0.105 * exp(0.218 * k);
let ct = 1 - 2 * rand2(j, k + 73.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(j, k + 75.7);
let rr = sr + 0.012 * (rand2(i, k + 4.2) - 0.5);
let x = sr + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.028;
}
glow(shellPink, 1);
cloud3(shellViolet, 10000, #c398ff, 0.065) {
let per = 5000;
let s = floor(i / per);
let j = i - s * per;
let k = s + 10;
let sr = 0.105 * exp(0.218 * k);
let ct = 1 - 2 * rand2(j, k + 83.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(j, k + 85.7);
let rr = sr + 0.012 * (rand2(i, k + 4.2) - 0.5);
let x = sr + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.030;
}
glow(shellViolet, 1);
cloud3(shellCyan, 16800, #b9ffff, 0.070) {
let per = 5600;
let s = floor(i / per);
let j = i - s * per;
let k = s + 12;
let sr = 0.105 * exp(0.218 * k);
let ct = 1 - 2 * rand2(j, k + 93.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(j, k + 95.7);
let rr = sr + 0.012 * (rand2(i, k + 4.2) - 0.5);
let x = sr + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.032;
}
glow(shellCyan, 1);
// Dense gold dust at the shared tangent becomes the white-hot crescent when
// viewed from the side and the tiny luminous bullseye when viewed end-on.
cloud3(junction, 7600, #ffd45a, 0.14) {
let ct = 1 - 2 * rand2(i, 103.1);
let st = sqrt(1 - ct * ct);
let th = tau * rand2(i, 105.7);
let rr = 0.095 * (0.45 + 0.55 * rand2(i, 71.2));
let x = 0.02 + rr * ct;
let y = rr * st * cos(th);
let z = rr * st * sin(th);
let r = 0.026;
}
glow(junction, 1);
orbit3(720, 0, 32, 20, linear);
shader-glitch-grid
A p5 WEBGL multi-pass sketch — a randomly generated grid pattern, RGB-shifted into a glitch —
reimagined as ONE per-pixel shader. The original pre-renders four grid/stripe layers into
off-screen buffers, composites them, captures the result, then a second shader tears it; manic
glsl() can’t sample render targets, but the OUTCOME is closed-form: build the nested random
grid procedurally per pixel (floor/fract/rand2), then chromatically tear it by sampling
each colour channel at a per-scanline horizontal offset. Pure in (u,v,t) — the glitch scrubs
and records exactly where the p5 sketch only draws once.
// shader-glitch-grid — a p5 WEBGL multi-pass sketch ("Glitch animation of a randomly
// generated grid pattern") reimagined in ONE manic `shader`. The original pre-renders
// FOUR grid/stripe layers into off-screen buffers, composites them with a substitution
// shader (each coarse cell shows a different sub-pattern), captures the result, then a
// second shader RGB-shifts it into a glitch. manic `glsl()` can't take render-target
// textures — but the OUTCOME is closed-form: build the nested grid PROCEDURALLY per
// pixel, then chromatically tear it by sampling each colour channel at a per-scanline
// horizontal offset. Pure in (u,v,t): the glitch scrubs and records exactly.
//
// manic examples/shader-glitch-grid.manic
title("Glitch grid — a multi-pass shader, reimagined per-pixel");
canvas("1:1");
template("black");
shader(glitch) {
// per-scanline-block horizontal offset, re-randomised a few times a second, and
// faded IN after the grid has settled (the original delays the glitch too)
let band = floor(v * 40.0);
let gt = floor(t * 3.0);
let gon = smoothstep(3.5, 4.5, t);
let off = (rand2(band, gt) - 0.5) * 0.06 * gon;
// RED — the nested random grid sampled at u + off
let ru = u + off;
let rcx = floor(ru * 10.0); let rcy = floor(v * 10.0); let rh = rand2(rcx, rcy);
let rdot = step(0.2, fract(ru * 100.0)) * step(fract(ru * 100.0), 0.8)
* step(0.2, fract(v * 100.0)) * step(fract(v * 100.0), 0.8);
let rstr = step(0.5, fract(v * 50.0));
let cr = mix(0.08, mix(mix(0.90, 0.12, rdot), mix(0.93, 0.18, rstr), step(0.7, rh)), step(0.4, rh));
// GREEN — same grid at u + off*0.3 (slight chromatic split)
let gu = u + off * 0.3;
let gcx = floor(gu * 10.0); let gh = rand2(gcx, rcy);
let gdot = step(0.2, fract(gu * 100.0)) * step(fract(gu * 100.0), 0.8)
* step(0.2, fract(v * 100.0)) * step(fract(v * 100.0), 0.8);
let cg = mix(0.08, mix(mix(0.90, 0.12, gdot), mix(0.93, 0.18, rstr), step(0.7, gh)), step(0.4, gh));
// BLUE — same grid at u + off*1.2 (the widest split)
let bu = u + off * 1.2;
let bcx = floor(bu * 10.0); let bh = rand2(bcx, rcy);
let bdot = step(0.2, fract(bu * 100.0)) * step(fract(bu * 100.0), 0.8)
* step(0.2, fract(v * 100.0)) * step(fract(v * 100.0), 0.8);
let cb = mix(0.08, mix(mix(0.90, 0.12, bdot), mix(0.93, 0.18, rstr), step(0.7, bh)), step(0.4, bh));
// white noise on top (as the original adds), stronger while glitching
let n = (rand2(u * 700.0 + gt, v * 700.0) - 0.5) * (0.05 + 0.12 * gon);
let r = cr + n;
let g = cg + n;
let b = cb + n;
}
caption(head, "Glitch grid — one formula per pixel", (400, 44), 22);
hidden(head);
show(head);
wait(9);
shader-plasma
A fragment-shader-style colour field — the per-PIXEL twin of cloud. Every pixel’s colour is ONE
closed-form formula of its normalized coords u/v, time t and aspect asp (shader(bg){ let r/g/b = … }), re-evaluated each frame yet pure in t so it scrubs and records exactly. manic’s take
on The Book of Shaders: layered travelling sines make plasma, a smoothstep vignette frames it — no
per-pixel loops, no assets, just algebra. The GLSL shaping idioms (mix/smoothstep/clamp/fract/
length) are now shared by every formula-driven builtin.
// shader-plasma — a fragment-shader-style colour field, the per-PIXEL twin of
// `cloud`. Every pixel's colour is ONE closed-form formula of its normalized
// coordinates `u`/`v`, live time `t`, and aspect `asp` — re-evaluated each frame
// yet pure in `t`, so it scrubs and records exactly. This is manic's take on
// "The Book of Shaders" (thebookofshaders.com): no per-pixel loops, just algebra.
//
// manic examples/shader-plasma.manic
title("A shader — one formula, every pixel");
canvas("9:16");
template("black");
shader(bg) {
// centre + aspect-correct so the field is round, not stretched (u/v are 0..1
// on BOTH axes, so a raw circle would be an ellipse on a 9:16 canvas).
let x = (u - 0.5) * asp;
let y = v - 0.5;
let d = length(x, y);
// layered travelling sines = classic plasma
let p = sin(x*7.0 + t) + sin(y*7.0 + t*1.3) + sin((x + y)*5.0 - t*0.9) + sin(d*11.0 - t*1.6);
// vignette: bright centre → dark edges (so the title/caption read in white)
let vig = smoothstep(0.95, 0.2, d);
let r = (0.5 + 0.5*sin(p + t)) * vig;
let g = (0.5 + 0.5*sin(p + t + 2.1)) * vig;
let b = (0.5 + 0.5*sin(p + t + 4.2)) * vig;
}
// ---- textbook annotations ----
caption(head, "A shader — one formula per pixel", (540, 150), 32);
caption(sub, "no loops, no assets — just algebra of (u, v, t)", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `\text{colour} = f(u,\, v,\, t)`, 40);
caption(lab, "per-pixel, re-evaluated every frame — yet seekable", (540, 1800), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
show(lab);
wait(24);
shader-fractal
A LIVING Julia set in a shader field. Each pixel iterates z→z²+c and colours by escape speed —
the Book of Shaders ‘Fractals’ chapter, with NO per-pixel loop in the DSL: julia(zx,zy,cx,cy) runs
the iteration in the engine and returns an escape fraction. Sweeping the constant c in a circle over
t morphs the fractal through the whole Julia family, every frame still a pure function of time. The
escape-hatch that also gives mandelbrot(x,y) and voronoi(x,y) (cellular noise) without loops.
// shader-fractal — a LIVING Julia set. Each pixel iterates z = z² + c a fixed
// number of times and colours by how fast it escapes — the Book-of-Shaders
// "Fractals" chapter, but with NO per-pixel loop in the DSL: `julia(zx,zy,cx,cy)`
// runs the iteration in the engine and returns an escape fraction in [0,1]. We
// sweep the constant `c` in a circle over time, so the fractal morphs through the
// whole Julia family — every frame still a pure function of `t` (scrub-safe).
//
// manic examples/shader-fractal.manic
title("A living Julia set — one formula per pixel");
canvas("9:16");
template("black");
shader(bg) {
// complex plane, aspect-corrected and centred
let zx = (u - 0.5) * 3.0 * asp;
let zy = (v - 0.5) * 3.0;
// the constant c orbits slowly → the set continuously morphs
let cx = 0.7 * cos(t * 0.35);
let cy = 0.7 * sin(t * 0.35);
let e = julia(zx, zy, cx, cy); // escape fraction: 1 = trapped, 0 = flees
let inside = step(0.985, e); // 1 for the fractal body
let band = 0.5 + 0.5 * sin(e * 26.0 - t * 2.0); // rainbow escape contours
let glow = 1.0 - inside; // dark body, lit exterior
let r = band * glow;
let g = (0.4 + 0.6 * band) * glow;
let b = (1.0 - 0.5 * band) * glow + inside * 0.06;
}
// ---- textbook annotations ----
caption(head, "A living Julia set", (540, 150), 34);
caption(sub, "z → z² + c, coloured by escape speed", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `z_{n+1} = z_n^2 + c`, 44);
caption(lab, "no per-pixel loop in the DSL — the engine iterates", (540, 1800), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
show(lab);
wait(24);
lost-in-patterns
A learning-forward fractal odyssey in four movements, each a different KIND of infinity with its
generating RULE shown on screen: ∞ by ZOOM — a ×500 Mandelbrot dive (z→z²+c); ∞ by PARAMETER
— the SAME rule with c the knob, wiring the Mandelbrot→Julia bridge (each c in the set grows one
Julia); ∞ by ITERATION — a Clifford strange attractor from one cloud … from map rule fed its own
output 6,000×; ∞ by RECURSION — a Koch snowflake (lsystem) closing on the paradox: perimeter
3(4/3)ⁿ→∞ yet area→8/5·A₀. Four generators (shader · shader · cloud · lsystem) as one lesson, pure in t.
// lost-in-patterns — a fractal odyssey in four movements, each a different KIND
// of infinity, each with the RULE that generates it shown on screen (manic's
// thesis: the diagram is true, so the maths is visible):
// I ∞ by ZOOM the Mandelbrot set — z→z²+c, a x500 dive into seahorse valley
// II ∞ by PARAMETER the Julia family — SAME rule, but now c is the knob
// (the Mandelbrot set is the MAP of which c give a connected Julia — the
// bridge between movements I and II)
// III ∞ by ITERATION a Clifford strange attractor — no shape-formula, one rule fed
// its own output 6,000× (cloud … from map)
// IV ∞ by RECURSION the Koch snowflake — F→F+F--F+F, closing on the paradox:
// infinite perimeter, finite area.
title("Lost in Infinite Patterns");
canvas("16:9");
template("black");
// ---------- HUD ----------
text(head, (cx, 60), "Lost in Infinite Patterns"); display(head); cursor(head); sticky(head);
text(cap, (cx, h - 42), ""); size(cap, 26); sticky(cap);
counter(mag, (w - 170, 120), 1, 0, "zoom x", ""); color(mag, gold); hidden(mag);
// a dark card so the teaching panel stays legible over the bright fractals
polygon(panel, (230, 92), (768, 92), (768, 250), (230, 250), #05070d);
opacity(panel, 0.5); sticky(panel); hidden(panel);
// the "kind of infinity" chip — one per movement (show/hide, no narration)
text(kind1, (499, 122), "∞ by ZOOM"); size(kind1, 30); color(kind1, cyan); sticky(kind1); hidden(kind1);
text(kind2, (499, 122), "∞ by PARAMETER"); size(kind2, 30); color(kind2, magenta); sticky(kind2); hidden(kind2);
text(kind3, (499, 122), "∞ by ITERATION"); size(kind3, 30); color(kind3, gold); sticky(kind3); hidden(kind3);
text(kind4, (499, 122), "∞ by RECURSION"); size(kind4, 30); color(kind4, lime); sticky(kind4); hidden(kind4);
// the generating rule of each movement — the maths made visible
equation(eqIter, (499, 190), `z_{n+1} = z_n^{2} + c`, 34); sticky(eqIter); hidden(eqIter);
equation(eqC, (499, 236), `c = 0.7885\,e^{i\theta}`, 26); sticky(eqC); hidden(eqC);
equation(eqCliff,(499, 196), `\begin{cases} x' = \sin(ay)+c\cos(ax) \\ y' = \sin(bx)+d\cos(by) \end{cases}`, 24); sticky(eqCliff); hidden(eqCliff);
equation(eqKoch, (499, 188), `F \to F\,{+}\,F\,{-}{-}\,F\,{+}\,F`, 30); sticky(eqKoch); hidden(eqKoch);
// the closing paradox — the one genuine gasp
equation(eqPar, (cx, h - 120), `\text{perimeter } 3\left(\tfrac{4}{3}\right)^{n}\!\to\infty \qquad \text{area}\to \tfrac{8}{5}A_0`, 34); sticky(eqPar); hidden(eqPar);
// hidden journey axes (the sliders stay backstage — the dive/angle drive the shaders)
parameter(dive, (w - 180, 660), 0, 0, 1, "dive", 2); hidden(dive);
parameter(ang, (w - 180, 690), 0, 0, 6.283, "angle", 2); hidden(ang);
// ---------- I. the Mandelbrot coastline ----------
shader(mset) {
let sc = 3.2 * exp(-6.2 * dive);
let x = -0.743644 + (u - 0.5) * asp * sc;
let y = 0.131826 + (v - 0.5) * sc;
let m = mandelbrot(x, y);
let band = m^0.35;
let hue = 205 + 140 * band + 25 * sin(0.3 * t);
let sat = 0.75;
let val = (1 - step(0.999, m)) * (0.12 + 0.88 * band);
}
// ---------- II. the Julia bloom ----------
shader(jul) {
let x = (u - 0.5) * asp * 2.9;
let y = (v - 0.5) * 2.9;
let cr = 0.7885 * cos(ang);
let ci = 0.7885 * sin(ang);
let j = julia(x, y, cr, ci);
let band = j^0.4;
let hue = 290 + 120 * band + 15 * sin(0.4 * t);
let sat = 0.8;
let val = (1 - step(0.999, j)) * (0.1 + 0.9 * band);
}
hidden(jul);
// ---------- III. the strange attractor (Clifford, a=-1.4 b=1.6 c=1 d=0.7) ----
cloud(att, 6000, #ffffff, 0.85) from map("sin(-1.4*y)+cos(-1.4*x)", "sin(1.6*x)+0.7*cos(1.6*y)", (0.1, 0.1)) {
let wsp = 0.12 * t;
let px = hx * cos(wsp) - hy * sin(wsp);
let py = hx * sin(wsp) + hy * cos(wsp);
let x = 640 + 225 * px; // cloud formulas can't see cx/cy — hardcode the 16:9 centre
let y = 360 + 15 + 165 * py;
let r = 1.1 + 0.8 * mod(abs(sin(i * 12.9898) * 43758.55), 1);
let hue = 150 + 60 * hypot(hx, hy) + 25 * sin(0.4 * t + 0.002 * i);
}
hidden(att);
// ---------- IV. the snowflake (Koch, one closed stroke) ----------
lsystem(koch, (cx, cy + 20), 560, "F--F--F", "F=F+F--F+F", "angle=60 iterations=4 closed=true");
untraced(koch); stroke(koch, 3); gradient(koch, cyan, magenta, gold);
// the four fullscreen visuals sit BEHIND the HUD (all default to z=0, so without
// this the later-declared shader would paint over the teaching panel + captions)
z(mset, -3); z(jul, -3); z(att, -3); z(koch, -2);
// ================= timeline =================
// ---- I: ∞ by ZOOM — the Mandelbrot dive
type(head, 1.2);
par { show(panel, 0.4); show(kind1, 0.4); show(mag, 0.3); }
show(eqIter, 0.6);
par {
to(dive, value, 1, 12, smooth);
to(mag, value, 500, 12, smooth);
seq {
say(cap, "the Mandelbrot set: keep the points where z → z² + c never flies to infinity", 0.6);
wait(4.6);
say(cap, "zoom 500x and the coastline keeps unfolding — new seahorses, never one exact repeat", 0.5);
wait(4.2);
}
}
cue(whoosh);
// ---- bridge: the Mandelbrot set IS the map of Julia sets
say(cap, "here is the secret: every point c in that black island grows its OWN fractal...", 0.5);
wait(3.2);
// ---- II: ∞ by PARAMETER — same rule, c is the knob
par { fade(mset, 1.2); fade(mag, 0.6); fade(kind1, 0.3); show(jul, 1.2); show(kind2, 0.4); }
show(eqC, 0.5); // eqIter STAYS on screen — same rule, bridging I and II
par {
to(ang, value, 6.283, 12, linear);
seq {
say(cap, "...a Julia set. Freeze z's rule, make c the knob: c inside the set → connected, outside → dust", 0.5);
wait(5.0);
say(cap, "slide c around a circle and every value is a different universe — same equation, new world", 0.5);
wait(4.6);
}
}
cue(whoosh);
// ---- III: ∞ by ITERATION — a rule fed its own output
par { fade(jul, 1.4); fade(kind2, 0.3); fade(eqIter, 0.3); fade(eqC, 0.3); show(att, 1.6); show(kind3, 0.4); }
show(eqCliff, 0.5);
say(cap, "no formula draws this shape — just this rule, fed its own output six thousand times", 0.5);
wait(4.0);
say(cap, "nudge one constant and it's a whole new creature: that sensitivity IS chaos", 0.5);
wait(3.4);
cue(whoosh);
// ---- IV: ∞ by RECURSION — the snowflake, and the paradox
par { fade(att, 1.2); fade(kind3, 0.3); fade(eqCliff, 0.3); show(kind4, 0.4); }
show(eqKoch, 0.5);
say(cap, "some infinities you draw with one stroke: replace every edge with four, forever", 0.5);
draw(koch, 6.0);
par { cam((cx + 215, cy - 100), 1.6, smooth); zoom(2.8, 1.6, smooth); }
say(cap, "look closer — the edge is made of smaller edges, at every scale", 0.5);
wait(2.6);
par { cam((cx, cy), 1.5, smooth); zoom(1, 1.5, smooth); }
// the payoff: infinite perimeter, finite area
show(eqPar, 0.8);
say(cap, "the perimeter grows x4/3 every step — to INFINITY — yet the whole shape fits in a circle", 0.6);
wait(3.4);
cue(chime);
say(cap, "four rules, four infinities — and you are never done looking", 0.7);
wait(3.2);
shader-shapes
The Book of Shaders ‘Shapes’ chapter in a shader field: draw with DISTANCE and ANGLE. length(x,y)
is the radius, atan2(y,x) the angle; modulating the radius by the angle turns a circle into a
breathing 5-point star, and smoothstep cuts a crisp anti-aliased edge from the distance field —
pure polar algebra of (u,v,t), no paths.
// shader-shapes — the Book-of-Shaders "Shapes" chapter: draw with DISTANCE and
// ANGLE. `length(x,y)` is the distance to the centre, `atan2(y,x)` the angle;
// modulating the radius by the angle turns a circle into a star or flower, and
// `smoothstep` cuts a crisp (anti-aliased) edge from the distance field. All
// per-pixel, no paths — just polar algebra of (u, v, t).
//
// manic examples/shader-shapes.manic
title("Shapes from distance & angle — a shader");
canvas("9:16");
template("black");
shader(bg) {
let x = (u - 0.5) * asp; // aspect-correct so it's round, not oval
let y = v - 0.5;
let r = length(x, y); // distance to centre
let a = atan2(y, x); // angle (polar)
// a 5-point star: the edge radius breathes with the angle (and pulses in t)
let edge = 0.30 + 0.08*cos(a*5.0 + t) + 0.02*sin(t*2.0);
let body = smoothstep(edge + 0.006, edge - 0.006, r); // 1 inside the star
let hue = mod(a*57.3 + t*30.0, 360.0); // angle → rainbow rim
let sat = 0.9;
let val = 0.12 + 0.6*body; // dark field, lit star
}
caption(head, "Shapes from distance & angle", (540, 150), 32);
caption(sub, "length() = radius, atan2() = angle — polar SDF", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `r=\text{length}(x,y),\quad \theta=\operatorname{atan2}(y,x)`, 30);
hidden(eq);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
wait(22);
shader-cellular
The ‘Cellular noise’ (Worley/Voronoi) chapter: voronoi(x,y) scatters one feature point per unit
cell and returns the distance to the nearest, so the field organises into flowing cells. The 3x3
neighbour search runs in the engine — the DSL formula stays closed-form and seekable, no loop.
// shader-cellular — the Book-of-Shaders "Cellular noise" (Worley/Voronoi)
// chapter. `voronoi(x,y)` scatters one feature point per unit cell and returns
// the distance to the NEAREST one — so the field organises into cells: bright
// near a point, dark at the equidistant borders. Drifting the coordinate in `t`
// makes the cells flow. The 3×3 neighbour search runs inside the engine, so the
// formula stays a closed-form, seekable function of (u, v, t) — no loop in the DSL.
//
// manic examples/shader-cellular.manic
title("Cellular noise — a Voronoi shader");
canvas("9:16");
template("black");
shader(bg) {
let s = 5.5;
let d = voronoi(u*s*asp, v*s + t*0.4); // distance to nearest cell point
let edge = smoothstep(0.0, 0.08, d); // ~0 at borders → dark cracks
let tb = smoothstep(0.0, 0.14, v) * smoothstep(1.0, 0.86, v); // darken top/bottom so captions read
let hue = mod(d*200.0 + t*24.0, 360.0); // colour by distance
let sat = 0.8;
let val = (0.15 + 0.7*edge) * tb;
}
caption(head, "Cellular noise", (540, 150), 34);
caption(sub, "distance to the nearest of many scattered points", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `F_1(p)=\min_i\;\lVert p - q_i\rVert`, 34);
hidden(eq);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
wait(22);
shader-patterns
The ‘Patterns’ chapter: fract+floor turn ONE tile into an infinite grid. fract(u*n) is the
position inside each tile, floor(u*n) is which tile — so one motif (a glowing disc + pulsing ring)
repeats everywhere and the tile index drives per-tile hue. No copies, no loop, just algebra.
// shader-patterns — the Book-of-Shaders "Patterns" chapter: `fract` and `floor`
// turn ONE cell into an infinite grid. `fract(u*n)` is the position INSIDE each
// tile (0..1), `floor(u*n)` is which tile you're in — so one motif drawn in tile
// space repeats everywhere, and the tile index can drive per-tile colour. Pure
// algebra of (u, v, t): no copies, no loop.
//
// manic examples/shader-patterns.manic
title("Patterns — one tile, endlessly repeated");
canvas("9:16");
template("black");
shader(bg) {
let n = 6.0;
let gx = fract(u*n*asp) - 0.5; // local coord inside each tile
let gy = fract(v*n) - 0.5;
let d = length(gx, gy);
let dot = smoothstep(0.36, 0.30, d); // a disc per tile
let ring = smoothstep(0.02, 0.0, abs(d - (0.24 + 0.06*sin(t)))); // pulsing ring
let id = floor(u*n*asp) + floor(v*n); // which tile → per-tile hue
let hue = mod(id*24.0 + t*36.0, 360.0);
let sat = 0.85;
let val = 0.12 + 0.6*dot + 0.5*ring;
}
caption(head, "Patterns from one tile", (540, 150), 34);
caption(sub, "fract() = position in tile, floor() = which tile", (540, 214), 20);
hidden(head);
hidden(sub);
equation(eq, (540, 1720), `\text{tile}(u)=\operatorname{fract}(u\,n),\ \operatorname{floor}(u\,n)`, 28);
hidden(eq);
show(head);
wait(1.4);
show(sub);
wait(2.4);
show(eq);
wait(22);
shader-shapes-dsl
Patricio’s glsl-shapes Shadertoy rebuilt as a FAITHFUL DSL twin using the shape/SDF builtin library:
each shape is ONE scalar call — sdpolygon/sdhexagon/sdstar/sdrhombus/sdroundbox — smin-
unioned and banded by an oscillating iso-line. True distance fields (no vec gymnastics), so the
concentric bands are exact offsets, not the polar approximation.
// shader-shapes-dsl — Patricio's glsl-shapes Shadertoy, now a FAITHFUL DSL twin using
// the new shape/SDF builtins (Layer 1). No vec2 algebra, no branches, no glsl — each
// shape is one scalar builtin (`sdpolygon`/`sdcircle`/`sdtriangle`/`sdhexagon`/
// `sdstar`/`sdrhombus`/`sdroundbox`), unioned, and banded by Patricio's oscillating
// iso-line. Transpiles to GLSL (full-res) with the CPU field as the exact fallback —
// so the SAME scene renders identically on both, unlike the old polar-approximation.
//
// manic examples/shader-shapes-dsl.manic
title("Patricio's shapes — faithful, in the shader DSL");
canvas("16:9");
template("black");
shader(shapes) {
let x = u * asp; // match Patricio's st = uv*vec2(asp,1)
let y = v;
// eight SDF shapes, each a single builtin (centres = Patricio's layout)
let s0 = sdpolygon(x - 0.48, y - 0.48, 0.076, 5); // pentagon
let s1 = sdcircle(x - 0.75, y - 0.80, 0.076); // circle
let s2 = sdtriangle(x - 0.21, y - 0.79, 0.076); // triangle
let s3 = sdpolygon(x - 0.16, y - 0.26, 0.076, 8); // octagon
let s4 = sdhexagon(x - 0.20, y - 0.50, 0.060); // hexagon
let s5 = sdstar(x - 0.79, y - 0.51, 0.11, 5); // 5-point star
let s6 = sdrhombus(x - 0.63, y - 0.17, 0.15, 0.07); // rhombus (≈ the ellipse)
let s7 = sdroundbox(x - 0.48, y - 0.79, 0.13, 0.05, 0.03); // rounded rectangle
// union — smin with a tiny k ≈ hard min (DSL `min` is a reduction, not callable)
let ua = smin(smin(smin(s0, s1, 0.003), s2, 0.003), s3, 0.003);
let ub = smin(smin(smin(s4, s5, 0.003), s6, 0.003), s7, 0.003);
let d = smin(ua, ub, 0.003);
// Patricio's oscillating iso-band + inside / thin-line masks
let band = floor(mod((d * 57.6 + t * 2.6) / 2.0, 1.0) * 2.0);
let outside = step(0.0, d);
let linem = step(0.0, d) * step(d, 0.006);
// two-tone palette: orange outside, blue inside (each banded); white iso-line
let br = mix(mix(0.431, 0.270, band), 1.000, outside);
let bgc = mix(mix(0.436, 0.190, band), mix(0.684, 0.514, band), outside);
let bbc = mix(1.000, mix(0.364, 0.128, band), outside);
let r = mix(br, 1.0, linem);
let g = mix(bgc, 1.0, linem);
let b = mix(bbc, 1.0, linem);
}
wait(8);
shader-fx
The shader/formula HELPER kit in action: fill(d,size,edge) and stroke(d,size,w,edge) turn an SDF
distance into a solid + a crisp outline, and gain shapes the gradient behind them. Clean vector-
style rendering from scalar builtins — SDF shapes + mask helpers, no glsl().
// shader-fx — the Layer-1 shader/formula helpers (fx kit) in action: `fill` and
// `stroke` turn an SDF distance into a solid + an outline, `gain` shapes a gradient.
// All scalar builtins, transpiled to GLSL with the CPU field as the exact fallback.
//
// manic examples/shader-fx.manic
title("fill · stroke · gain — SDF mask helpers");
canvas("16:9");
template("black");
shader(fx) {
let x = u * asp;
let y = v;
// three shapes, unioned
let s0 = sdhexagon(x - 0.60, y - 0.5, 0.15);
let s1 = sdstar(x - 1.05, y - 0.5, 0.17, 5);
let s2 = sdcircle(x - 1.48, y - 0.5, 0.13);
let d = smin(smin(s0, s1, 0.01), s2, 0.01);
// masks from the distance
let fl = fill(d, 0.0, 0.004); // 1 inside → 0 outside
let ol = stroke(d, 0.0, 0.03, 0.004); // bright band on the iso-line
// gain-shaped vertical gradient behind the shapes
let bgv = gain(v, 2.2);
let base = 0.10 + 0.20 * bgv;
// compose: gradient bg, teal fill, white outline
let r = mix(mix(base, 0.16, fl), 1.0, ol);
let g = mix(mix(base, 0.52, fl), 1.0, ol);
let b = mix(mix(base * 1.7, 0.62, fl), 1.0, ol);
}
wait(4);
shader-parameter
A shader driven by a scene parameter, not just by time: the ring frequency is a SLIDER the field
references by name, so the SAME shader re-renders as you sweep it. On the GPU it’s a u_freq uniform;
on the deterministic CPU fallback the live value is substituted into the formula — both in lock-step.
// shader-parameter — a `shader` driven by a scene `parameter`, not just by time.
// The ring frequency `freq` is a slider/parameter: the shader references it by
// name, so the SAME field re-renders as `freq` animates. On the GLSL path it's a
// `u_freq` uniform (resolved from the parameter each frame); on the CPU fallback
// the live value is substituted into the formula — both stay in lock-step.
//
// manic examples/shader-parameter.manic
title("A shader driven by a parameter");
canvas("16:9");
template("black");
parameter(freq, (640, 660), 3, 1, 14, "freq", 0);
shader(rings) {
let d = hypot((u - 0.5) * asp, v - 0.5); // distance from centre (aspect-correct)
let hue = mod(d * freq * 90.0, 360.0); // ring hue cycles faster as freq rises
let sat = 0.8;
let val = 0.55 + 0.35 * sin(d * freq * 18.0);
}
caption(head, "shader ← parameter", (640, 66), 34);
hidden(head);
show(head);
// sweep the parameter: the rings tighten as freq climbs 3 → 14
to(freq, value, 14, 6, smooth);
shader-warp
DOMAIN WARPING — the class of shader Layer-1 builtins can’t express, because you can’t rotate a scalar
coordinate. With vec2 + rot2 + swizzle you rotate SPACE itself (more the further from centre):
let p = vec2(...); let q = rot2(p, ang); … q.x … q.y. Real vector maths in the DSL — vectors are
erased to scalar Nodes at compile, so the swirl runs on both backends from one source.
// shader-warp — Layer 2: vec math in the DSL. The headline that Layer-1 builtins
// can't express — DOMAIN WARPING. You can't rotate a scalar coordinate; with vec2 +
// rot2 + swizzle you rotate SPACE itself (more the further from centre), then read a
// ring pattern in the warped frame. Pure DSL — no glsl(), no vec2 gymnastics beyond
// the builtins. vecs are erased to scalar math at compile, so it renders on both
// backends (CPU field + GLSL) from one source.
//
// manic examples/shader-warp.manic
title("Domain warp — vec2 + rot2 in the shader DSL");
canvas("16:9");
template("black");
shader(warp) {
let p = vec2(u * asp - 0.9, v - 0.5); // name the centred coordinate as a vec2
let ang = length(p) * 7.0 - t; // twist grows with radius
let q = rot2(p, ang); // rotate SPACE once; reuse the vec2
let hue = mod(200.0 + q.x * 500.0 + q.y * 300.0, 360.0);
let sat = 0.8;
let val = 0.5 + 0.4 * sin(q.y * 40.0);
}
wait(8);
raymarch-metaballs
Shader V2: a 3-D scene RAY-MARCHED per pixel. You write only the signed-distance field let d
(distance from any point x,y,z to the scene); the engine marches a ray per pixel to the surface,
takes the normal by finite differences and shades it — the per-pixel loop runs in the engine (like
voronoi/mandelbrot), and there are NO vec/mat types (the SDF is a scalar formula, component math
the manic way). Three spheres orbit and MERGE through smin (smooth union) into living metaballs.
// raymarch-metaballs — Shader V2: a 3-D scene RAY-MARCHED per pixel. You write
// only the signed-distance field `let d` (the distance from any point x,y,z to
// the scene); the engine marches a ray per pixel until it hits the surface,
// takes the normal by finite differences, and shades it. No per-pixel loop in
// the DSL (it runs in the engine, like `voronoi`/`mandelbrot`) and NO vec/mat
// types — the SDF is a scalar formula, component math the manic way. Here three
// spheres orbit and MERGE through `smin` (smooth union) into living metaballs.
//
// manic examples/raymarch-metaballs.manic
title("Metaballs — a ray-marched 3D field");
canvas("16:9");
template("black");
raymarch(blobs) {
// three moving spheres (signed distance = distance to centre − radius)
let a = sdsphere(x - 0.75*sin(t), y - 0.5*cos(t*1.3), z + 0.3*sin(t*0.7), 0.52);
let b = sdsphere(x + 0.6*cos(t*0.9), y + 0.45*sin(t*1.1), z - 0.35*cos(t), 0.46);
let c = sdsphere(x + 0.2*sin(t*1.7), y + 0.6*sin(t*0.7), z + 0.25*sin(t*1.4), 0.4);
// smooth-union them (smin) so they gloop together instead of just overlapping
let ab = smin(a, b, 0.55);
let d = smin(ab, c, 0.55);
}
// ---- textbook annotations ----
caption(head, "Metaballs — one distance field", (640, 66), 34);
caption(sub, "raymarch: you write the SDF, the engine marches it", (640, 122), 22);
hidden(head);
hidden(sub);
equation(eq, (640, 648), `d = \operatorname{smin}(d_1, d_2, k)`, 34);
hidden(eq);
show(head);
wait(1.6);
show(sub);
wait(2.6);
show(eq);
wait(22);
raymarch-sculpture
Shader V2.2: a coloured, carved SDF sculpture that a real camera3 orbits. New over the
metaballs: your own HIT colour (let hue/sat/val or r/g/b, a formula of the surface normal
nx/ny/nz, hit height hz and time t); camera3 reuse so orbit3 sweeps the scene; and the
SDF boolean toolkit — smin (smooth union), sdsub (carve a shape out), sdint (intersect). Still
one scalar distance field — no per-pixel loop, no vec types.
// raymarch-sculpture — Shader V2.2: a coloured, carved SDF sculpture that a real
// `camera3` orbits. New since V2.1: (1) your own HIT colour — `let hue`/`sat`/`val`
// (or r/g/b) as a formula of the surface normal `nx`/`ny`/`nz`, hit height `hz`
// and time `t`; (2) `camera3` reuse — the marcher builds its rays from the scene
// camera, so `orbit3` sweeps around the scene; (3) SDF booleans `smin` (smooth
// union), `sdsub` (carve), `sdint` (intersect). Still just a scalar distance
// field — no per-pixel loop in the DSL, no vec types.
//
// manic examples/raymarch-sculpture.manic
title("A carved, coloured SDF — orbited by camera3");
canvas("16:9");
template("black");
camera3((3.4, -3.8, 2.2), (0, 0, 0.15), 38, perspective);
raymarch(gem) {
// a core sphere with two bumps smoothly fused on (metaball style)
let core = sdsphere(x, y, z, 1.0);
let b1 = sdsphere(x - 0.9*sin(t*0.8), y, z + 0.9*cos(t*0.8), 0.44);
let b2 = sdsphere(x + 0.5*cos(t), y - 0.85*sin(t*1.1), z, 0.4);
let blob = smin(smin(core, b1, 0.45), b2, 0.45);
// carve a spherical bite out of it
let bite = sdsphere(x - 0.55, y - 0.9, z + 0.55, 0.62);
let d = sdsub(bite, blob);
// iridescent colour: hue from the facing direction + a slow time sweep
let hue = mod(205.0 + nx*95.0 + ny*55.0 + t*40.0, 360.0);
let sat = 0.82;
let val = 0.52 + 0.28*nz;
}
caption(head, "A carved, coloured SDF", (640, 66), 34);
caption(sub, "hit colour from the normal + camera3 orbit", (640, 122), 22);
hidden(head);
hidden(sub);
show(head);
wait(1.4);
show(sub);
// slow camera orbit — the marcher re-reads camera3 every frame (captions stay up)
orbit3(70, 0, 5.4, 24, smooth);
raymarch-boxgrid
Shader V2.3, the finale: the raymarched Shadertoy that started the thread (tssSDN — a grid of
boxes rippling around a bouncing sphere), rebuilt as a TRUE ray-march (not the cloud3 reimagining).
One SDF: rep(x,r) tiles a box into an infinite grid (sdbox3), each cell’s HEIGHT a wave of its
rand2 hash + a falloff from the moving sphere, smin-unioned with the sphere. Coloured red→gold by
height, orbited by camera3 — the per-pixel march runs in the engine, no vec types, one formula.
// raymarch-boxgrid — Shader V2.3, the finale: the raymarched Shadertoy that
// started this whole thread (tssSDN — a grid of boxes rippling around a bouncing
// sphere), rebuilt as a TRUE ray-march this time (V2 tier), not the cloud3
// reimagining (examples/cloud3-ripple.manic). One signed-distance field does it:
// `rep(x,r)` tiles a box into an infinite grid (`sdbox3`), each cell's HEIGHT a
// wave of its `rand2` hash + a falloff from the moving sphere's position; `smin`
// unions in the sphere. Per-pixel loop in the ENGINE, no vec types — just a
// scalar distance formula. Coloured red→gold by height, orbited by `camera3`.
//
// manic examples/raymarch-boxgrid.manic
title("A ray-marched box grid — the Shadertoy, rebuilt");
canvas("16:9");
template("black");
camera3((2.6, -3.2, 2.2), (0, 0, 0.25), 40, perspective);
raymarch(grid) {
let rp = 0.42; // cell size
let idx = floor(x / rp); // which cell (x)
let idy = floor(y / rp); // which cell (y)
let lx = rep(x, rp); // local coord inside the cell
let ly = rep(y, rp);
let sx = sin(t * 1.8) * 1.3; // the bouncing sphere (inlined)
let sy = cos(t * 2.2) * 1.3;
let cxx = idx * rp + rp * 0.5; // this cell's centre
let cyy = idy * rp + rp * 0.5;
let bs = hypot(cxx - sx, cyy - sy); // cell → sphere distance
let fall = 1 - smoothstep(0.0, 2.2, bs); // near the sphere ⇒ taller
let hsh = rand2(idx, idy); // per-cell phase
let bh = 0.34 + 0.30 * sin(hsh * 6.283 + t * 2.6 + bs * 1.7) * fall; // box height (>0)
let box = sdbox3(lx, ly, z - bh * 0.5, rp * 0.4, rp * 0.4, bh * 0.5);
let ball = sdsphere(x - sx, y - sy, z - 0.6, 0.18);
let d = smin(box, ball, 0.04); // grid ∪ sphere
// colour: red troughs → gold crests, top faces brighter
let hue = mod(6.0 + bh * 42.0, 360.0);
let sat = 0.85;
let val = 0.28 + 0.55 * nz;
}
caption(head, "A ray-marched box grid", (640, 66), 34);
caption(sub, "sdbox3 + rep() tiling + smin — the Shadertoy, in manic", (640, 122), 22);
hidden(head);
hidden(sub);
show(head);
wait(1.5);
show(sub);
orbit3(50, 0, 4.6, 22, smooth);
raymarch-docker-latency
Data as geometry: Docker daemon socket latency as a raymarched LIQUID MESH. The daemon sits at
the origin emitting high-frequency concentric pings (amplitude ∝ a jittery round-trip-time signal);
three containers fire expanding ring events at baked timestamps, each ring’s reach ∝ its measured
RTT. Every cell of a sdbox3+rep() grid samples that field at its centre, so the mesh shimmers
with socket traffic — one scalar SDF, marched by the engine, coloured by hit height. Honest by
design: manic is pure in t, so the trace is BAKED (not a live socket) — swap the constants for a
captured docker events log and it replays deterministically. Data → SDF displacement → raymarch.
// raymarch-docker-latency — "Visualizing Docker daemon socket latency as a raymarched
// fluid surface." A per-pixel ray-marched LIQUID MESH: a grid of columns whose heights
// ARE a latency trace. The daemon socket sits at the origin and emits high-frequency
// concentric pings (amplitude modulated by a jittery round-trip-time signal); three
// containers fire expanding ring events at baked timestamps, each ring's reach ∝ its
// measured RTT. Every cell samples that field at its centre → the mesh shimmers with
// socket traffic. One scalar SDF (`sdbox3` + `rep()` tiling), marched by the engine.
//
// Honest note: manic is PURE IN t (that's what lets it scrub + record), so it does NOT
// tail a live /var/run/docker.sock in real time. The trace is BAKED IN — timestamps and
// RTTs as constants — so the same second always renders the same wavefront. Swap the
// constants for a captured `docker events` / socket-latency log and the mesh replays it
// deterministically: data → SDF displacement → raymarch, exactly as described. The data
// source is a recording, not a socket; the mechanism is real.
//
// manic examples/raymarch-docker-latency.manic
title("Docker daemon socket latency — a raymarched liquid mesh");
canvas("16:9");
template("black");
camera3((2.4, -3.3, 2.0), (0, 0, 0.2), 40, perspective);
raymarch(fluid) {
let rp = 0.34; // mesh cell size
let idx = floor(x / rp); let idy = floor(y / rp);
let lx = rep(x, rp); let ly = rep(y, rp);
let cx = idx*rp + rp*0.5; let cy = idy*rp + rp*0.5; // this cell's centre
let r0 = hypot(cx, cy); // distance from the daemon socket (origin)
// baked latency signal: socket round-trip time, jittery + bursty
let lat = 0.5 + 0.28*sin(t*5.3) + 0.16*sin(t*11.7 + 1.3) + 0.10*sin(t*23.1 + 0.7);
// the daemon socket: high-frequency concentric pings, amplitude ∝ latency
let pings = lat * sin(6.0*r0 - t*7.0) / (1.0 + 1.3*r0);
// three containers talking to the daemon: baked (epicenter, fire time, RTT) rings
let d1 = hypot(cx + 1.3, cy - 0.8); let a1 = t - 1.4; let f1 = a1*1.9;
let e1 = step(0.0, a1) * exp(-0.7*a1) * sin(7.0*(d1 - f1)) * exp(-3.0*(d1-f1)*(d1-f1));
let d2 = hypot(cx - 1.6, cy - 1.1); let a2 = t - 3.2; let f2 = a2*2.1;
let e2 = step(0.0, a2) * exp(-0.6*a2) * sin(7.0*(d2 - f2)) * exp(-3.0*(d2-f2)*(d2-f2));
let d3 = hypot(cx + 0.4, cy + 1.7); let a3 = t - 5.0; let f3 = a3*2.0;
let e3 = step(0.0, a3) * exp(-0.55*a3) * sin(7.0*(d3 - f3)) * exp(-3.0*(d3-f3)*(d3-f3));
// column height = calm water level + the summed latency displacement (always > 0)
let bh = clamp(0.22 + 0.13*pings + 0.17*(e1 + e2 + e3), 0.03, 0.78);
let box = sdbox3(lx, ly, z - bh*0.5, rp*0.42, rp*0.42, bh*0.5);
let d = box;
// hit colour: deep-blue troughs → bright cyan crests (from the actual hit height),
// top faces brightest — no cross-stage lets, so the field colours cleanly
let crest = clamp(hz * 1.7, 0.0, 1.0);
let hue = mod(210.0 - crest*56.0, 360.0);
let sat = 0.82;
let val = 0.16 + 0.55*crest + 0.30*nz;
}
// ---- annotations ----
caption(head, "Docker daemon socket latency", (640, 60), 33);
caption(sub, "each socket ping ripples a raymarched liquid mesh", (640, 112), 21);
hidden(head);
hidden(sub);
equation(eq, (640, 636), `z_{\text{cell}} = \mathrm{water} + \sum_i \mathrm{RTT}_i\,\mathrm{ring}(r_i - c\,\Delta t_i)`, 26);
caption(note, "baked latency trace → SDF displacement → raymarch · pure in t, so it scrubs", (640, 690), 18);
hidden(eq);
hidden(note);
show(head);
wait(1.6);
show(sub);
wait(2.2);
show(eq);
show(note);
// slow orbit so the mesh reads as genuine 3-D geometry
orbit3(52, 8, 4.8, 22, smooth);
ssl-handshake-sdf
A TLS 1.3 handshake rendered as a raymarched SDF scene, message by message. Four scene
parameters are the reactive shader variables the timeline drives: entropy boils every surface,
x25519 key agreement smin-FUSES the two endpoint solids into a secret neither side ever sent, HKDF
sprouts a key TREE, and the encrypted channel becomes a rep()-tiled tunnel of cipher rings. All
ALL-SCALAR (branch tilts as component rotations, no vec2/rot2) so the marcher runs on the GPU.
Honest by design: pure in t, a deterministic replay of one captured handshake — not a live socket.
// ssl-handshake-sdf — a TLS 1.3 handshake rendered as a raymarched SDF scene.
// Four scene parameters are the "reactive shader variables"; the timeline
// replays the handshake by driving them, and the SDF reads them by name:
// ent - cryptographic entropy -> surface displacement (the boil)
// mrg - x25519 key agreement -> the two endpoint solids smin-FUSE
// grw - HKDF key-schedule -> a key TREE grows from the shared secret
// tun - the encrypted channel -> rep()-tiled cipher rings, an endless tunnel
// Honesty note: manic is a pure function of t - no live sockets. This is a
// deterministic replay of one captured handshake; the hex in the transcript is
// that capture, not live traffic. The marcher is fully GPU-transpilable (scalar
// SDF, no vec lets), so it previews and records smoothly.
title("The Handshake, Made Visible");
canvas("16:9");
template("black");
// ---------- HUD ----------
text(head, (cx, 60), "The Handshake, Made Visible"); display(head); cursor(head);
text(cap, (cx, h - 42), ""); size(cap, 26);
equation(eqk, (cx, 170), `(g^{a})^{b} \;=\; (g^{b})^{a}`, 36); hidden(eqk);
// the transcript, typed line by line (terminal-green, top left)
text(tl1, (330, 150), "ClientHello random: 9f3a c241 77d0 8e5b"); size(tl1, 24); color(tl1, lime); cursor(tl1); hidden(tl1);
text(tl2, (330, 190), "ServerHello random: 4be7 01cc a913 f2d6"); size(tl2, 24); color(tl2, lime); cursor(tl2); hidden(tl2);
text(tl3, (330, 230), "KeyShare x25519: e5a2 39f8 1b44 c07e"); size(tl3, 24); color(tl3, lime); cursor(tl3); hidden(tl3);
text(tl4, (330, 270), "HKDF-Expand client + server traffic keys"); size(tl4, 24); color(tl4, lime); cursor(tl4); hidden(tl4);
text(tl5, (330, 310), "Finished cipher: CHACHA20-POLY1305"); size(tl5, 24); color(tl5, lime); cursor(tl5); hidden(tl5);
// entropy readouts (the visible reactive variable + a bit counter)
counter(bits, (w - 185, 118), 0, 0, "entropy bits ", ""); color(bits, gold); hidden(bits);
parameter(ent, (w - 180, 168), 0.02, 0, 1, "entropy", 2);
parameter(mrg, (w - 180, 238), 0, 0, 1, "key-mix", 2); hidden(mrg);
parameter(grw, (w - 180, 308), 0, 0, 1, "hkdf", 2); hidden(grw);
parameter(tun, (w - 180, 378), 0, 0, 1, "tunnel", 2); hidden(tun);
// ---------- the scene ----------
camera3((4.5, -5.5, 3.4), (0, 0, 1.1), 44);
raymarch(hs) {
// entropy boils every surface: high-frequency displacement scaled by `ent`
let wob = ent * 0.13 * sin(6*x + 2*t) * sin(6*y + 1.7*t) * sin(6*z + 1.3*t);
// the two endpoints: browser (sphere) and server (octahedron), pulled
// together as the key-mix parameter rises
let ox = 1.25 - 0.95*mrg;
let c = sdsphere(x + ox, y, z - 0.85, 0.55) + wob;
let s = sdoctahedron(x - ox, y, z - 0.85, 0.62) + wob;
let duo = smin(c, s, 0.12 + 0.55*mrg);
// the HKDF key tree: trunk + branches, growing out of the fused secret as `grw`
// rises. Branch tilts are SCALAR component rotations (not vec2/rot2) so the whole
// marcher transpiles to GLSL and runs on the GPU instead of the heavy CPU fallback.
// rot(a,b,θ) = (a·cosθ − b·sinθ, a·sinθ + b·cosθ)
let zt = z - 0.85;
let tr = sdcapsule(x, y, zt, 1.05*grw, 0.12);
let za = zt - 1.0*grw; let ca = cos(0.65); let sa = sin(0.65);
let zb = zt - 1.55*grw; let cb = cos(0.8); let sb = sin(0.8);
let b1 = sdcapsule(x*ca - za*sa, y, x*sa + za*ca, 0.7*grw, 0.085);
let b2 = sdcapsule(x*ca + za*sa, y, 0 - x*sa + za*ca, 0.7*grw, 0.085);
let b3 = sdcapsule(x, y*cb - zb*sb, y*sb + zb*cb, 0.5*grw, 0.06);
let b4 = sdcapsule(x, y*cb + zb*sb, 0 - y*sb + zb*cb, 0.5*grw, 0.06);
let tree0 = smin(smin(tr, smin(b1, b2, 0.1), 0.12), smin(b3, b4, 0.1), 0.12);
let tree = tree0 + 0.6*wob + (1 - smoothstep(0.02, 0.12, grw))*9;
// the encrypted tunnel: an endless procession of cipher rings along y
let ry = rep(y, 1.05);
let ring = sdtorus(x, z - 0.85, ry, 0.8, 0.05 + 0.03*sin(3*t + y)) + (1 - smoothstep(0.02, 0.2, tun))*9;
let d = smin(smin(duo, tree, 0.14), ring, 0.1);
// colour: cool protocol teal, warmed and destabilized by entropy
let hue = 165 + 55*nz + 30*sin(2*hz + 0.5*t) + 50*ent;
let sat = 0.7;
let val = 0.85 + 0.15*nz;
let alpha = 1;
}
// ================= timeline: the handshake, message by message ==============
type(head, 1.1);
say(cap, "a TLS 1.3 handshake - captured once, replayed as geometry", 0.6);
wait(0.8);
say(cap, "two strangers: your browser, and a server it has never met", 0.5);
wait(1.4);
// ---- ClientHello: 32 bytes of randomness leave home
cue(tick);
show(tl1, 0.1); type(tl1, 1.0);
show(bits, 0.3);
say(cap, "ClientHello: 32 bytes of pure randomness leave home - the surface begins to boil", 0.5);
par { to(ent, value, 0.55, 1.6, smooth); to(bits, value, 256, 1.6); orbit3(-35, 20, 7.4, 1.6, smooth); }
wait(0.8);
// ---- ServerHello: chaos answers chaos
cue(tick);
show(tl2, 0.1); type(tl2, 1.0);
say(cap, "ServerHello: the server answers with chaos of its own", 0.5);
par { to(ent, value, 0.85, 1.4, smooth); to(bits, value, 512, 1.4); }
wait(0.8);
// ---- x25519: the fusion - a secret neither of them ever sent
cue(whoosh);
show(tl3, 0.1); type(tl3, 1.0);
show(eqk, 0.6);
say(cap, "x25519: the shapes fuse into a secret that NEITHER side ever transmitted", 0.5);
par { to(mrg, value, 1, 2.4, smooth); orbit3(15, 24, 6.8, 2.4, smooth); }
wait(0.9);
// ---- HKDF: the key tree
cue(pop);
show(tl4, 0.1); type(tl4, 1.0);
say(cap, "HKDF: one shared secret sprouts a whole tree of session keys", 0.5);
par { to(grw, value, 1, 2.6, smooth); orbit3(60, 26, 6.6, 2.6, smooth); }
wait(0.9);
// ---- Finished: the tunnel opens
cue(chime);
show(tl5, 0.1); type(tl5, 1.0);
say(cap, "Finished: from here on, every byte travels dressed in noise", 0.5);
par { to(tun, value, 1, 2.2, smooth); to(ent, value, 1, 2.2, smooth); orbit3(110, 18, 7.2, 4.5, smooth); }
disintegrate(eqk, 0.8);
say(cap, "the invisible negotiation, rendered visible", 0.6);
wait(2.5);
type-to-sdf
Typography, dismantled across all three shader tiers in one scene. Act 1 — 2,600 points born
INSIDE the glyphs of “SDF” via cloud(…) from text("SDF"), swarming out and flying home. Act 2 —
the letters re-authored as ray-marched signed-distance VOLUMES (raymarch, kept ALL-SCALAR so it
transpiles to the GPU — tori carved by box intersections, no vec2/rot2). Act 3 — the SAME
three sliders (weight/wave/melt) drive a raw glsl pass. One interface, three ways manic renders a
field: per-point cloud, per-pixel scalar raymarch, and hand-written GLSL.
// type-to-sdf — flat 2D typography dismantled into a point field, rebuilt as
// reactive 3D signed-distance volumes, then handed to raw GLSL.
// act 1 cloud ... from text("SDF") - the glyphs dissolve into positions
// act 2 raymarch, ALL-SCALAR - S, D, F re-authored as SDF volumes
// (scalar-only => GPU transpile; parameters bind as uniforms;
// tori are carved with box INTERSECTIONS, no rot2/vec2 anywhere)
// act 3 glsl - the SAME sliders drive raw GLSL
// Three UI parameters are the whole interface: weight / wave / melt.
title("Type, Dismantled");
canvas("16:9");
template("black");
// ---------- HUD ----------
text(head, (cx, 60), "Type, Dismantled: Flat Glyphs to SDF Volumes"); display(head); cursor(head);
text(cap, (cx, h - 42), ""); size(cap, 26);
// the UI: three sliders, visibly driving everything
parameter(wgt, (w - 180, 150), 0.15, 0, 1, "weight", 2);
parameter(wav, (w - 180, 220), 0, 0, 1, "wave", 2);
parameter(mlt, (w - 180, 290), 0, 0, 1, "melt", 2);
// ---------- act 1: the glyph field ----------
// 2600 points born INSIDE the glyphs of "SDF"; each remembers home (hx, hy).
// They swarm out at t~3.2 and fly home at t~6 - the word dismantled and recalled.
cloud(dust, 2600, #ffffff, 0.9) from text("SDF") {
let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
let sc = smoothstep(3.2, 4.6, t) - smoothstep(6.0, 7.6, t);
let ang = i * 2.399;
let rad = 40 + 230 * rn;
let x = hx + sc * rad * cos(ang + 0.5 * t);
let y = hy + sc * rad * sin(ang + 0.5 * t) * 0.8;
let r = 1.6 + 1.4 * rn;
let hue = 190 + 50 * sin(0.7 * i + t);
}
// ---------- act 2: the volumes ----------
camera3((0, -4.6, 1.6), (0, 0, 0.95), 40);
raymarch(vol) {
// domain warp: the WAVE slider bends the coordinate field itself
let xw = x + wav * 0.16 * sin(2.6 * z + 2 * t);
let zw = z + wav * 0.10 * sin(2.2 * x - 1.6 * t);
// ---- S (at x = -1.5): two torus arcs, quadrants removed by intersection
let sx = xw + 1.5;
let torU = sdtorus(sx, zw - 1.15, y, 0.26, 0.09);
let torL = sdtorus(sx, zw - 0.63, y, 0.26, 0.09);
let upC = smin(sdint(torU, sdbox3(sx + 0.30, y, zw - 1.15, 0.32, 0.6, 0.45)), sdint(torU, sdbox3(sx, y, zw - 1.32, 0.6, 0.6, 0.20)), 0.02);
let loC = smin(sdint(torL, sdbox3(sx - 0.30, y, zw - 0.63, 0.32, 0.6, 0.45)), sdint(torL, sdbox3(sx, y, zw - 0.44, 0.6, 0.6, 0.20)), 0.02);
let dS = smin(upC, loC, 0.05);
// ---- D (at x = 0): stem + right half of a ring
let stemD = sdbox3(xw + 0.26, y, zw - 0.9, 0.09, 0.10, 0.55);
let bowl = sdint(sdtorus(xw + 0.10, zw - 0.9, y, 0.36, 0.09), sdbox3(xw - 0.22, y, zw - 0.9, 0.34, 0.6, 0.6));
let dD = smin(stemD, bowl, 0.05);
// ---- F (at x = +1.5): stem + two arms
let fx = xw - 1.5;
let stemF = sdbox3(fx + 0.22, y, zw - 0.9, 0.09, 0.10, 0.55);
let armT = sdbox3(fx, y, zw - 1.36, 0.30, 0.10, 0.09);
let armM = sdbox3(fx - 0.03, y, zw - 0.98, 0.24, 0.10, 0.08);
let dF = smin(stemF, smin(armT, armM, 0.03), 0.05);
// ---- the word: MELT widens the union until letterforms dissolve;
// WEIGHT is one sdround inflation - bold is literally deeper
let k = 0.05 + 0.5 * mlt;
let word = smin(smin(dS, dD, k), dF, k);
let fluid = mlt * 0.03 * sin(5 * xw + 2.4 * t) * sin(4 * zw - 1.8 * t);
let d = sdround(word, 0.02 + 0.09 * wgt) + fluid;
let hue = 190 + 40 * nx + 50 * mlt + 20 * sin(3 * hz + t);
let sat = 0.75;
let val = 0.9 + 0.1 * nz;
let alpha = 1;
}
hidden(vol);
// ---------- act 3: the same sliders, raw GLSL ----------
glsl(neon, `
uniform float u_wgt;
uniform float u_mlt;
float smin2(float a, float b, float k){ float h = clamp(0.5 + 0.5*(b - a)/k, 0.0, 1.0); return mix(b, a, h) - k*h*(1.0 - h); }
void mainImage(out vec4 O, in vec2 I){
vec2 p = (2.0*I - iResolution.xy)/iResolution.y;
float k = 0.15 + 0.6*u_mlt;
float d = 1e5;
for (int i = 0; i < 3; i++) {
float fi = float(i);
vec2 c = vec2(-0.9 + 0.9*fi, 0.12*sin(iTime*1.3 + fi*2.1));
float r = 0.26 + 0.12*u_wgt + 0.05*sin(iTime*2.0 + fi);
d = smin2(d, length(p - c) - r, k);
}
float glow = pow(0.02/max(abs(d), 0.004), 0.9);
vec3 col = glow * mix(vec3(0.1, 0.8, 1.0), vec3(1.0, 0.4, 0.9), 0.5 + 0.5*sin(3.0*u_mlt + p.x));
O = vec4(col, clamp(glow, 0.0, 1.0));
}
`);
hidden(neon);
// ================= timeline =================
// ---- act 1: flat, then dismantled
type(head, 1.1);
say(cap, "this is type as your screen stores it: flat outlines, frozen in place", 0.6);
wait(1.4);
cue(whoosh);
say(cap, "step one: DISMANTLE - a glyph is nothing but a field of positions", 0.5);
wait(2.6);
say(cap, "...and every position remembers home", 0.5);
wait(2.6);
// ---- act 2: rebuilt as signed-distance volumes
cue(whoosh);
par { fade(dust, 0.9); show(vol, 1.2); }
say(cap, "step two: REBUILD - each letter re-authored as a signed distance volume", 0.5);
orbit3(-24, 16, 5.0, 2.0, smooth);
// weight: variable font, one number
cue(tick);
say(cap, "one slider inflates every stroke: WEIGHT - bold is literally deeper", 0.5);
to(wgt, value, 1, 1.6, smooth);
to(wgt, value, 0.35, 1.2, smooth);
// wave: the coordinate field bends
cue(tick);
say(cap, "WAVE bends the coordinate field itself - the letters ride it", 0.5);
par { to(wav, value, 1, 1.8, smooth); orbit3(18, 22, 4.8, 1.8, smooth); }
to(wav, value, 0.35, 1.2, smooth);
// melt: typography as fluid
cue(pop);
say(cap, "MELT widens the union - and the word forgets its letterforms", 0.5);
par { to(mlt, value, 1, 2.6, smooth); orbit3(40, 14, 5.2, 2.6, smooth); }
wait(1.0);
// ---- act 3: the bridge to raw GLSL
cue(chime);
say(cap, "step three: the SAME sliders, handed to raw GLSL at full resolution", 0.5);
par { fade(vol, 1.0); show(neon, 1.0); }
to(mlt, value, 0.25, 1.6, smooth);
par { to(mlt, value, 0.9, 1.8, smooth); to(wgt, value, 0.8, 1.8, smooth); }
to(wgt, value, 0.3, 1.4, smooth);
say(cap, "flat vectors in - living volumes out", 0.6);
wait(2.5);
raymarch-shapes3
The 3-D SDF PRIMITIVES in a real ray-march: a torus, an octahedron and a capsule spindle —
sdtorus/sdoctahedron/sdcapsule — smooth-unioned and orbited by camera3. Each is ONE scalar
builtin (no hand-written distance functions), full-resolution GLSL with the CPU marcher as the exact
fallback.
// raymarch-shapes3 — the 3-D shape/SDF builtins (Layer 1, batch 2) in a real
// ray-march: a torus, an octahedron, and a capsule spindle, smooth-unioned and
// orbited by camera3. Each is ONE scalar builtin — no vec math, no hand-written
// distance functions. Full-res GLSL with the CPU marcher as the exact fallback.
//
// manic examples/raymarch-shapes3.manic
title("Ray-marched SDF primitives — torus · octahedron · capsule");
canvas("16:9");
template("black");
camera3((3.2, -3.6, 2.4), (0, 0, 0), 40, perspective);
raymarch(scene) {
let tor = sdtorus(x, y, z, 0.95, 0.26); // ring in the XY plane
let oct = sdoctahedron(x, y, z - 1.05, 0.5); // floating above
let spn = sdcapsule(x, y, z, 1.5, 0.10); // vertical spindle through it
let d = smin(smin(tor, oct, 0.18), spn, 0.12);
// iridescent hit colour from the normal + a slow time sweep
let hue = mod(190.0 + nz * 70.0 + nx * 40.0 + t * 30.0, 360.0);
let sat = 0.82;
let val = 0.52 + 0.32 * nz;
}
caption(head, "sdtorus · sdoctahedron · sdcapsule", (640, 66), 30);
caption(sub, "each shape one scalar builtin — no vec math", (640, 118), 20);
hidden(head);
hidden(sub);
show(head);
wait(1.4);
show(sub);
orbit3(70, 0, 5.4, 20, smooth);
raymarch-alpha
OUTPUT ALPHA / compositing: a raymarched metaball with let alpha renders on a TRANSPARENT background,
so the object floats over the rest of the scene (here a full-canvas shader gradient) instead of an
opaque backdrop. Faithful on both backends — GLSL writes vec4(rgb, alpha), the CPU field stores
per-texel alpha and composites the same way.
// raymarch-alpha — Shader backbone step 7: OUTPUT ALPHA. A raymarched object with
// `let alpha` renders on a TRANSPARENT background, so it composites over the rest of
// the scene instead of drawing an opaque backdrop. Here a coloured metaball floats
// over a full-canvas 2-D `shader` gradient — you can see the gradient THROUGH the
// object's missed rays (and faintly through the object itself at alpha 0.9). Alpha
// is faithful on both backends: GLSL writes `vec4(rgb, alpha)` and blends; the CPU
// field stores per-texel alpha and composites the same way.
//
// manic examples/raymarch-alpha.manic
title("Raymarch with alpha — compositing over the scene");
canvas("16:9");
template("black");
// a colourful 2-D shader gradient BEHIND the object (fills the canvas, opaque)
shader(back) {
let hue = mod(u * 160.0 + v * 90.0 + 20.0, 360.0);
let sat = 0.7;
let val = 0.42 + 0.12 * sin(u * 6.28);
}
camera3((2.6, -3.2, 2.0), (0, 0, 0), 40, perspective);
raymarch(blob) {
// two spheres smooth-unioned into a metaball
let a = sdsphere(x - 0.55 * sin(t), y, z + 0.4 * cos(t), 0.52);
let b = sdsphere(x + 0.55 * cos(t * 0.8), y - 0.25, z, 0.44);
let d = smin(a, b, 0.4);
// iridescent hit colour
let hue = mod(205.0 + nx * 85.0 + t * 34.0, 360.0);
let sat = 0.82;
let val = 0.55 + 0.3 * nz;
// OUTPUT ALPHA: hit ⇒ 0.9 (slightly translucent), miss ⇒ transparent (composites)
let alpha = 0.9;
}
caption(head, "raymarch ← alpha (composited)", (640, 66), 32);
hidden(head);
show(head);
orbit3(60, 0, 5.0, 18, smooth);
glsl-raymarch
Shader V3: glsl(id, "shader source") runs a REAL GLSL fragment shader (Shadertoy-style mainImage,
iTime/iResolution/iMouse) straight through the graphics pipeline — so the whole scene is marched
PER PIXEL by the GPU/GL: crisp, anti-aliased, full resolution, and fast. The SAME shader runs on Metal
(Mac), llvmpipe (headless prod, JIT to CPU) and WebGL (browser). This is the path to true Shadertoy
quality in manic — paste a shader, it just runs.
// glsl-raymarch — Shader V3: run a REAL GLSL fragment shader, full resolution.
// `glsl(id, `<shader>`)` hands a Shadertoy-style `mainImage` straight to the GPU
// (macroquad material) — so the whole scene is marched PER PIXEL by the graphics
// pipeline: crisp, anti-aliased, and fast, with `iTime`/`iResolution`/`iMouse`
// uniforms. The same shader runs on Metal (Mac), llvmpipe (headless prod, JIT to
// CPU), and WebGL (browser). This is the path to true Shadertoy quality in manic
// — paste a shader, it just runs.
//
// manic examples/glsl-raymarch.manic
title("A real GLSL raymarch — full resolution");
canvas("16:9");
template("black");
glsl(scene, `
float sdSphere(vec3 p, float r){ return length(p) - r; }
float map(vec3 p){
vec3 q = p;
q.xy = mod(q.xy + 1.0, 2.0) - 1.0; // infinite grid
float bob = 0.18 * sin(iTime*1.6 + p.x*0.8 + p.y*0.7);
return sdSphere(q - vec3(0.0, 0.0, bob), 0.42);
}
vec3 nrm(vec3 p){
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(map(p+e.xyy)-map(p-e.xyy),
map(p+e.yxy)-map(p-e.yxy),
map(p+e.yyx)-map(p-e.yyx)));
}
void mainImage(out vec4 o, in vec2 fc){
vec2 uv = (fc*2.0 - iResolution)/min(iResolution.x, iResolution.y);
vec3 ro = vec3(sin(iTime*0.3)*0.7, cos(iTime*0.25)*0.5, 3.0);
vec3 rd = normalize(vec3(uv, -1.6));
float t = 0.0; float hit = 0.0;
for(int i=0;i<110;i++){
vec3 p = ro + rd*t;
float d = map(p);
if(d < 0.001){ hit = 1.0; break; }
t += d;
if(t > 24.0) break;
}
vec3 col = vec3(0.04, 0.05, 0.08);
if(hit > 0.5){
vec3 p = ro + rd*t;
vec3 n = nrm(p);
float diff = max(dot(n, normalize(vec3(0.6, 0.8, 0.5))), 0.0)*0.7 + 0.3;
vec3 base = 0.5 + 0.5*cos(iTime + p.xyx*0.6 + vec3(0.0, 2.0, 4.0)); // iridescent
col = base * diff;
}
o = vec4(pow(col, vec3(0.4545)), 1.0);
}
`);
caption(head, "A real GLSL raymarch", (640, 66), 34);
caption(sub, "glsl(...) runs a fragment shader per pixel — full res, fast", (640, 122), 22);
hidden(head);
hidden(sub);
show(head);
wait(1.6);
show(sub);
wait(24);
glsl-boxgrid
The ACTUAL Shadertoy (tssSDN — a grid of boxes rippling around a bouncing sphere) that started this
whole thread, running in manic UNCHANGED via glsl. The CPU rebuild (raymarch-boxgrid) traded
resolution for the no-GPU farm; this hands the real fragment shader — DDA cell-stepping raymarch,
face-coloured boxes and all — straight to the graphics pipeline, so it renders PER PIXEL at full
resolution (~0.08s a frame on Mac). Paste a shader, it just runs — same code on Mac, prod and browser.
// glsl-boxgrid — the ACTUAL Shadertoy (tssSDN) that started this whole thread,
// running in manic UNCHANGED via the `glsl` V3 path. Our CPU rebuild
// (examples/raymarch-boxgrid.manic) traded resolution for the no-GPU farm; this
// hands the real fragment shader straight to the graphics pipeline, so it renders
// PER PIXEL at full resolution — GPU-marched on Mac, JIT-to-CPU on llvmpipe prod,
// WebGL in the browser. Same shader, three targets. This IS capture.webm.
//
// Original shader: https://www.shadertoy.com/view/tssSDN (ukeyshima)
// manic examples/glsl-boxgrid.manic
canvas("16:9");
template("black");
glsl(scene, `
#define MAX_DIST 1000.
#define SURF_DIST .0001
#define EPS .0001
#define PI 3.141592
#define PI2 PI*2.
#define REP(p,r) mod(p,r) - r * .5
float rand(vec2 co){ return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453); }
float sdSphere(vec3 p, float s){ return length(p) - s; }
float sdBox(vec3 p, vec3 b){ vec3 q = abs(p) - b; return length(max(q, 0.)) + min(max(q.x, max(q.y, q.z)), 0.); }
vec2 minMat(vec2 d1, vec2 d2){ return (d1.x < d2.x) ? d1 : d2; }
float rep = .04;
vec2 scene(vec3 p){
vec2 d = vec2(100000., 0.);
float t = iTime;
vec3 q = p;
vec3 spo = vec3(sin(t * 1.8) * .25, .32, cos(t * 2.2) * .3);
vec3 sp = q - spo;
d.x = sdSphere(sp, .075);
vec2 id = floor(q.xz / rep);
float hash = rand(id * .001);
q.xz = mod(q.xz, rep) - rep * .5;
vec3 bcp = vec3(0.);
bcp.xz = id * rep + rep * .5;
float bsDist = length(spo.xz - bcp.xz);
float s = smoothstep(0., .5, bsDist);
q -= vec3(0., .125 - (sin(hash * PI2 + t * (2. + bsDist * .015)) * .05) * (1. - pow(s, .9)), 0.);
d = minMat(d, vec2(sdBox(q, vec3(rep * .5, .1, rep * .5)), 1.));
return d;
}
vec3 getNormal(vec3 p){
vec2 e = vec2(EPS, 0.);
return normalize(vec3(scene(p + e.xyy).x - scene(p - e.xyy).x,
scene(p + e.yxy).x - scene(p - e.yxy).x,
scene(p + e.yyx).x - scene(p - e.yyx).x));
}
vec2 raymarch(vec3 ro, vec3 rd, float side){
float accDist = 0.; float mat = 0.;
for(int i = 0; i < 128; i++){
vec3 p = ro + rd * accDist;
vec2 result = scene(p);
float dist = result.x * side;
vec3 rdi = 1. / rd;
mat = result.y;
if(abs(dist) < SURF_DIST || accDist > MAX_DIST) break;
accDist += min(min((step(0., rd.x) - mod(p.x, rep)) * rdi.x,
(step(0., rd.z) - mod(p.z, rep)) * rdi.z) + .0001, dist);
}
return vec2(accDist, mat);
}
vec3 getRayDir(vec2 uv, vec3 p, vec3 l, float z){
vec3 forward = normalize(l - p);
vec3 right = normalize(cross(forward, vec3(0., 1., 0.)));
vec3 up = normalize(cross(right, forward));
return normalize(right * uv.x + up * uv.y + forward * z);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord){
float t = iTime;
vec2 uv = (fragCoord.xy * 2. - iResolution.xy) / min(iResolution.x, iResolution.y);
vec3 ro = vec3(1., 1., 1.2);
vec3 ta = vec3(0., .2, 0.);
vec3 rd = getRayDir(uv, ro, ta, 3.5);
vec2 result = raymarch(ro, rd, 1.);
float dist = result.x; float mat = result.y;
vec3 col = vec3(0.);
if(dist < MAX_DIST){
vec3 p = ro + rd * dist;
vec3 l = normalize(vec3(1., 1., -1.));
vec3 n = getNormal(p);
float diffuse = dot(l, n) * .5 + .5;
vec3 diffuseColor = vec3(diffuse);
if(mat < .5){ diffuseColor *= vec3(1., 0., 0.); }
else {
diffuseColor *= vec3(1.);
if(n.x > .5) diffuseColor = diffuse * vec3(1., 0., 0.);
if(n.y > .5) diffuseColor = diffuse * vec3(1., .9, .9);
if(n.z > .5) diffuseColor = diffuse * vec3(.6, 0., 0.);
}
col = diffuseColor;
}
col = pow(col, vec3(.4545));
fragColor = vec4(col, 1.);
}
`);
wait(12);
glsl-shapes
Patricio Gonzalez Vivo’s 2015 ‘shapes’ Shadertoy running in manic UNCHANGED via glsl — a 2-D SDF
gallery (pentagon/circle/triangle/octagon/hexagon/star/ellipse/rounded-rect) banded by an oscillating
iso-line. Paste the real fragment shader, it just runs at full resolution, animated by iTime. The
DSL twin (shader-shapes-dsl) shows the other path to the same look.
// glsl-shapes — Patricio Gonzalez Vivo's 2015 "shapes" Shadertoy, running in manic
// UNCHANGED via the raw glsl() V3 path (2-D SDF gallery: pentagon/sphere/triangle/
// octagon/hexagon/star/ellipse/rounded-rect, banded by an oscillating iso-line).
// Original: http://patriciogonzalezvivo.com · @patriciogv
canvas("16:9");
template("black");
glsl(scene, `
float osc(float d){
return floor(mod((d+iTime*2.600)/2.,1.)*2.);
}
float sphere(vec2 p, float r){
return length(p)-r;
}
float tri( in vec2 p, in float r ){
const float k = sqrt(3.0);
p.x = abs(p.x) - r;
p.y = p.y + r/k;
if( p.x+k*p.y>0.0 ) p = vec2(p.x-k*p.y,-k*p.x-p.y)/2.0;
p.x -= clamp( p.x, -2.0*r, 0.0 );
return -length(p)*sign(p.y);
}
float pent( in vec2 p, in float r ){
const vec3 k = vec3(0.809016994,0.587785252,0.726542528);
p.x = abs(p.x);
p -= 2.0*min(dot(vec2(-k.x,k.y),p),0.0)*vec2(-k.x,k.y);
p -= 2.0*min(dot(vec2( k.x,k.y),p),0.0)*vec2( k.x,k.y);
p -= vec2(clamp(p.x,-r*k.z,r*k.z),r);
return length(p)*sign(p.y);
}
float oct( in vec2 p, in float r ){
const vec3 k = vec3(-0.9238795325, 0.3826834323, 0.4142135623 );
p = abs(p);
p -= 2.0*min(dot(vec2( k.x,k.y),p),0.0)*vec2( k.x,k.y);
p -= 2.0*min(dot(vec2(-k.x,k.y),p),0.0)*vec2(-k.x,k.y);
p -= vec2(clamp(p.x, -k.z*r, k.z*r), r);
return length(p)*sign(p.y);
}
float hex( in vec2 p, in float r ){
const vec4 k = vec4(-0.5,0.8660254038,0.5773502692,1.7320508076);
p = abs(p);
p -= 2.0*min(dot(k.xy,p),0.0)*k.xy;
p -= 2.0*min(dot(k.yx,p),0.0)*k.yx;
p -= vec2(clamp(p.x,r*k.z,r*k.w),r);
return length(p)*sign(p.y);
}
float str(in vec2 p, in float r, in float rf){
const vec2 k1 = vec2(0.809016994375, -0.587785252292);
const vec2 k2 = vec2(-k1.x,k1.y);
p.x = abs(p.x);
p -= 2.0*max(dot(k1,p),0.0)*k1;
p -= 2.0*max(dot(k2,p),0.0)*k2;
p.x = abs(p.x);
p.y -= r;
vec2 ba = rf*vec2(-k1.y,k1.x) - vec2(0,1);
float h = clamp( dot(p,ba)/dot(ba,ba), 0.0, r );
return length(p-ba*h) * sign(p.y*ba.x-p.x*ba.y);
}
float elp( in vec2 p, in vec2 ab ){
p = abs(p); if( p.x > p.y ) {p=p.yx;ab=ab.yx;}
float l = ab.y*ab.y - ab.x*ab.x;
float m = ab.x*p.x/l; float m2 = m*m;
float n = ab.y*p.y/l; float n2 = n*n;
float c = (m2+n2-1.0)/3.0; float c3 = c*c*c;
float q = c3 + m2*n2*2.0;
float d = c3 + m2*n2;
float g = m + m*n2;
float co;
if( d<0.0 ){
float h = acos(q/c3)/3.0;
float s = cos(h);
float t = sin(h)*sqrt(3.0);
float rx = sqrt( -c*(s + t + 2.0) + m2 );
float ry = sqrt( -c*(s - t + 2.0) + m2 );
co = (ry+sign(l)*rx+abs(g)/(rx*ry)- m)/2.0;
} else {
float h = 2.0*m*n*sqrt( d );
float s = sign(q+h)*pow(abs(q+h), 1.0/3.0);
float u = sign(q-h)*pow(abs(q-h), 1.0/3.0);
float rx = -s - u - c*4.0 + 2.0*m2;
float ry = (s - u)*sqrt(3.0);
float rm = sqrt( rx*rx + ry*ry );
co = (ry/sqrt(rm-rx)+2.0*g/rm-m)/2.0;
}
vec2 r = ab * vec2(co, sqrt(1.0-co*co));
return length(r-p) * sign(p.y-r.y);
}
float rnd( in vec2 p, in float w, in float r ){
p = abs(p);
return length(p-min(p.x+p.y,w)*0.5) - r;
}
vec2 cent = vec2(0.480,0.480);
float line = 0.008;
float spac = 0.0;
void mainImage( out vec4 fragColor, in vec2 fragCoord ){
vec2 st = fragCoord/iResolution.xy * vec2( iResolution.x/iResolution.y, 1. );
vec3 col = vec3(1.0);
float d = pent(st-cent,0.076);
d = min(d,sphere(st-vec2(0.750,0.800),0.076));
d = min(d,tri(st-vec2(0.210,0.790),0.076));
d = min(d,oct(st-vec2(0.160,0.260),0.076));
d = min(d,hex(st-vec2(0.200,0.500),0.036));
d = min(d,str(st-vec2(0.790,0.510),0.108,0.640));
d = min(d,elp(st-vec2(0.630,0.170),vec2(0.280,0.100)));
d = min(d,rnd(st-vec2(0.480,0.790),0.284,0.032));
float band = osc(d*200.*0.288);
if(d > spac && d < spac+line){
col = vec3(1.);
} else if(d > 0.0){
col*= vec3(1.000,0.684,0.364)*(1.-band)+vec3(1.000,0.514,0.128)*band;
} else {
col*= vec3(0.431,0.436,1.000)*(1.-band)+vec3(0.270,0.190,1.000)*band;
}
fragColor = vec4(col,1.0);
}
`);
wait(8);
glsl-parameter
A RAW GLSL shader driven by a scene parameter: declare uniform float u_freq; in the paste and it
AUTO-BINDS to the slider freq, so a raw Shadertoy gets a manic control with no wrapper changes.
Same uniform table the DSL paths use — declaring the camera basis uniform vec3 iCamEye; binds to
camera3 the same way, so orbit3 can sweep a pasted shader too.
// glsl-parameter — a RAW GLSL shader driven by a scene `parameter`. Step 5 of the
// shader backbone: `glsl()` is now a full citizen of the uniform table, so a paste
// that DECLARES `uniform float u_<name>;` auto-binds to the matching `parameter`
// (and `uniform vec3 iCamEye;` etc. would bind to `camera3`). Here `u_freq` is a
// slider: the same shader re-renders as you sweep it — no re-paste, no edits.
//
// manic examples/glsl-parameter.manic
title("A raw GLSL shader, driven by a slider");
canvas("16:9");
template("black");
parameter(freq, (640, 660), 3, 1, 16, "freq", 0);
glsl(scene, `
uniform float u_freq;
void mainImage(out vec4 o, in vec2 fc){
vec2 uv = fc / iResolution;
vec2 c = uv - 0.5;
c.x *= iResolution.x / iResolution.y; // aspect-correct
float d = length(c);
float rings = 0.5 + 0.5 * sin(d * u_freq * 32.0 - iTime);
vec3 col = vec3(rings, 1.0 - rings, 0.5 + 0.5 * sin(d * u_freq * 8.0));
o = vec4(col, 1.0);
}
`);
caption(head, "glsl ← parameter", (640, 66), 34);
hidden(head);
show(head);
to(freq, value, 16, 6, smooth);
bloom-scope
Multi-pass BLOOM, dissected live. A ‘light scope’ (reticle ring + ticks + core + orbiting emitters +
a pulsing probe) in raw glsl, with the bloom pipeline broken into inspectable passes a view slider
steps through: the crisp scene, the bright-pass (luminance over threshold τ — the ticks vanish), the
gaussian stack (N passes, radius DOUBLING each), and the additive composite. No ping-pong buffers
needed: every emitter is an SDF, so each blurred pass is closed-form exp(-d²/σ²) and the mip ladder
is one constant-bound loop. Five sliders bind straight in as u_<name> uniforms; the flicker beat
shows a hard threshold POP the soft knee fixes.
// bloom-scope — multi-pass bloom, dissected live. A "light scope" (reticle
// ring + ticks + core + three orbiting emitters + one pulsing probe) rendered
// in raw GLSL, with the bloom pipeline broken into inspectable passes:
// view 0 the crisp scene (no bloom)
// view 1 bright-pass: luminance over the threshold tau (ticks vanish!)
// view 2 the gaussian stack: N passes, radius DOUBLING each pass
// view 3 composite: scene + sum of passes (additive - base stays crisp)
// manic has no ping-pong buffers - and doesn't need them here: every emitter
// is an SDF, so each blurred pass exists in closed form exp(-d^2/sigma^2);
// the mip ladder becomes one constant-bound loop. Five UI sliders bind
// straight into the shader as u_<name> uniforms. The flicker beat: the probe
// pulses across tau - hard threshold makes its halo POP; the soft knee fixes it.
title("Bloom, One Pass at a Time");
canvas("16:9");
template("black");
// ---------- HUD ----------
text(head, (cx, 60), "Bloom, One Pass at a Time"); display(head); cursor(head);
text(cap, (cx, h - 42), ""); size(cap, 26);
text(viewlab, (300, h - 100), ""); size(viewlab, 24); color(viewlab, lime);
equation(eqb, (cx, 150), `\text{bloom} \;=\; \sum_{p=0}^{N} w_p\; G_{2^{p}\sigma}\!\big(\max(L-\tau,\,0)\big)`, 32); hidden(eqb);
// the UI: five sliders, bound into the shader as u_glo/u_rad/u_pas/u_kne/u_vew
parameter(glo, (w - 180, 140), 0, 0, 2, "intensity", 2);
parameter(rad, (w - 180, 205), 0.5, 0, 2, "radius", 2);
parameter(pas, (w - 180, 270), 0, 0, 5, "passes", 1);
parameter(kne, (w - 180, 335), 0, 0, 1, "knee", 2);
parameter(vew, (w - 180, 400), 0, 0, 3, "view", 1);
counter(cnt, (w - 185, 462), 0, 0, "active passes ", ""); color(cnt, gold); hidden(cnt);
// ---------- the shader ----------
glsl(scope, `
uniform float u_glo;
uniform float u_rad;
uniform float u_pas;
uniform float u_kne;
uniform float u_vew;
float sdRing(vec2 p, float R){ return abs(length(p) - R); }
float sdBox(vec2 p, vec2 b){ vec2 d = abs(p) - b; return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0); }
// bright-pass weight: hard threshold (tau = 0.55) blended toward a soft knee
float knee(float b){ return mix(step(0.55, b), smoothstep(0.25, 0.70, b), u_kne); }
// the gaussian stack: N passes, sigma doubling each pass, weights decaying -
// the closed-form mip ladder (no texture taps: d comes from the SDF)
vec3 halo(float d, vec3 cb, float w){
vec3 a = vec3(0.0);
for (int pp = 0; pp < 5; pp++) {
float on = clamp(u_pas - float(pp), 0.0, 1.0);
float s = max(0.02, u_rad * 0.05 * pow(2.0, float(pp)));
a += on * w * cb * exp(-(d*d)/(s*s)) * 0.6 / pow(1.6, float(pp));
}
return a * u_glo;
}
void mainImage(out vec4 O, in vec2 I){
vec2 p = (2.0*I - iResolution.xy)/iResolution.y;
// ---- the light scope: reticle ring + folded tick marks + core
float dRing = sdRing(p, 0.62);
vec2 q = vec2(abs(p.x), abs(p.y));
float ticks = min(sdBox(vec2(q.x - 0.62, p.y), vec2(0.055, 0.012)),
sdBox(vec2(p.x, q.y - 0.62), vec2(0.012, 0.055)));
float dCore = length(p) - 0.035;
// ---- three orbiting emitters + the pulsing probe (the flicker demo)
float a1 = 0.7*iTime;
vec2 c1 = 0.45*vec2(cos(a1), sin(a1));
vec2 c2 = 0.45*vec2(cos(a1 + 2.09), sin(a1 + 2.09));
vec2 c3 = 0.45*vec2(cos(a1 + 4.19), sin(a1 + 4.19));
float o1 = length(p - c1) - 0.050;
float o2 = length(p - c2) - 0.040;
float o3 = length(p - c3) - 0.045;
float dPr = length(p - vec2(0.0, -0.86)) - 0.055;
float bPr = 0.35 + 0.45*(0.5 + 0.5*sin(2.6*iTime));
vec3 cRing = vec3(0.30, 0.85, 1.00);
vec3 cCore = vec3(1.00, 0.95, 0.80);
vec3 cO1 = vec3(1.00, 0.72, 0.25);
vec3 cO2 = vec3(1.00, 0.35, 0.80);
vec3 cO3 = vec3(0.30, 1.00, 0.75);
vec3 cPr = vec3(0.75, 0.85, 1.00);
// ---- view 0: the crisp scene
float e = 0.006;
vec3 scene = vec3(0.0);
scene += smoothstep(e, 0.0, dRing) * cRing * 0.77;
scene += smoothstep(e, 0.0, ticks) * cRing * 0.50;
scene += smoothstep(e, 0.0, dCore) * cCore * 1.00;
scene += smoothstep(e, 0.0, o1) * cO1 * 0.95;
scene += smoothstep(e, 0.0, o2) * cO2 * 0.90;
scene += smoothstep(e, 0.0, o3) * cO3 * 0.90;
scene += smoothstep(e, 0.0, dPr) * cPr * bPr;
// ---- view 1: bright extraction (the 0.50 reticle ticks fall BELOW tau)
vec3 bright = vec3(0.0);
bright += smoothstep(e, 0.0, dRing) * cRing * knee(0.77);
bright += smoothstep(e, 0.0, dCore) * cCore * knee(1.00);
bright += smoothstep(e, 0.0, o1) * cO1 * knee(0.95);
bright += smoothstep(e, 0.0, o2) * cO2 * knee(0.90);
bright += smoothstep(e, 0.0, o3) * cO3 * knee(0.90);
bright += smoothstep(e, 0.0, dPr) * cPr * knee(bPr);
// ---- views 2/3: the stacked gaussian passes
vec3 bloom = vec3(0.0);
bloom += halo(dRing, cRing * 0.77, knee(0.77) * 0.55);
bloom += halo(dCore, cCore, knee(1.00));
bloom += halo(o1, cO1 * 0.95, knee(0.95));
bloom += halo(o2, cO2 * 0.90, knee(0.90));
bloom += halo(o3, cO3 * 0.90, knee(0.90));
bloom += halo(dPr, cPr * bPr, knee(bPr));
// ---- the view dial
float vA = 1.0 - clamp(abs(u_vew - 0.0), 0.0, 1.0);
float vB = 1.0 - clamp(abs(u_vew - 1.0), 0.0, 1.0);
float vC = 1.0 - clamp(abs(u_vew - 2.0), 0.0, 1.0);
float vD = 1.0 - clamp(abs(u_vew - 3.0), 0.0, 1.0);
vec3 col = vA*scene + vB*bright + vC*bloom + vD*(scene + bloom);
float aOut = clamp(1.6*max(col.r, max(col.g, col.b)), 0.0, 1.0);
O = vec4(col, aOut);
}
`);
// ================= timeline =================
// ---- view 0: honest and dead
type(head, 1.1);
say(viewlab, "VIEW 0 - scene, no bloom", 0.3);
say(cap, "a light scope, rendered honest: crisp, correct - and completely dead", 0.6);
wait(2.2);
// ---- view 1: the bright-pass
cue(tick);
say(viewlab, "VIEW 1 - bright-pass: L > tau", 0.3);
to(vew, value, 1, 0.8, smooth);
say(cap, "pass one: keep only what outshines the threshold - the fine ticks vanish", 0.5);
wait(2.6);
// ---- view 2: stack the gaussian passes, one by one
cue(whoosh);
say(viewlab, "VIEW 2 - gaussian stack, radius x2 per pass", 0.3);
to(vew, value, 2, 0.8, smooth);
show(eqb, 0.6);
show(cnt, 0.3);
to(glo, value, 1, 0.8, smooth);
say(cap, "now blur it - again and again, radius doubling: the mip ladder", 0.5);
par { to(pas, value, 1, 0.7, smooth); to(cnt, value, 1, 0.7); }
wait(0.4);
par { to(pas, value, 2, 0.7, smooth); to(cnt, value, 2, 0.7); }
wait(0.4);
par { to(pas, value, 3, 0.7, smooth); to(cnt, value, 3, 0.7); }
wait(0.4);
par { to(pas, value, 5, 1.0, smooth); to(cnt, value, 5, 1.0); }
say(cap, "no ping-pong buffers: the emitters are SDFs, so every pass exists in closed form", 0.5);
wait(1.2);
// ---- view 3: the composite
cue(chime);
say(viewlab, "VIEW 3 - composite: scene + all passes", 0.3);
to(vew, value, 3, 0.9, smooth);
say(cap, "add it back on top: the scope begins to BLEED - but the base layer stays crisp", 0.5);
wait(2.0);
// ---- the flicker, and the fix
cue(tick);
say(cap, "watch the probe at the bottom: a HARD threshold makes its halo pop on and off", 0.5);
wait(3.4);
say(cap, "the fix: a soft KNEE - brightness eases across tau, and the halo breathes", 0.5);
to(kne, value, 1, 1.6, smooth);
wait(3.0);
// ---- finale: crank it
cue(whoosh);
say(cap, "clarity amidst chaos: bloom is additive light, never a blur of the scene", 0.5);
par { to(glo, value, 1.7, 2.0, smooth); to(rad, value, 1.5, 2.0, smooth); breathe(eqb, 3, 0.05, 0, 6); }
to(rad, value, 0.9, 1.6, smooth);
say(cap, "five sliders, one shader, every pass inspectable", 0.6);
wait(2.5);
tree-of-leaves
A seed becomes a tree over one year — entirely from cloud particles. A wood front grows the trunk
and branches, then 5,500 golden-angle leaves POP (spring), flutter, turn autumn, FALL in a gust and
fade into the soil, then RETURN green; blossoms open and shed petal-snow. 8,420 points, one small
formula each, every one a pure function of index and time (scrub-safe, no state).
// tree-of-leaves — the full lifecycle cut. One seed, one year, one clock.
// Everything is still a pure function of index i and time t (scrub-safe, no
// state): four clouds now tell a complete story --
// stars (220) - twinkling night sky
// wood (1,500) - trunk + five branches, grown by a front after the seed lands
// leaves (5,500) - golden-angle canopy; each leaf POPS (spring), flutters,
// turns autumn-coloured on its own clock, FALLS in the gust,
// fades into the soil over winter... and RETURNS, green again
// bloom (1,200) - blossoms that open on the young canopy, shed their petals
// in a slow petal-snow, and bloom once more at the end
// 8,420 points - one small formula each.
// Idioms: max(0,s) = 0.5*(s+abs(s)); min(a,b) = a - max(0, a-b);
// hash(i) = mod(abs(sin(i*12.9898)*43758.55), 1)
//
// manic tree-of-leaves.manic
title("A Seed, a Tree, a Year");
canvas("9:16");
template("black");
// ---------- HUD ----------
text(head, (540, 150), "A Seed, a Tree, a Year"); display(head); cursor(head);
text(season, (540, 330), ""); size(season, 34); color(season, lime);
text(cap, (540, 1800), ""); size(cap, 30);
counter(nlv, (540, 255), 0, 0, "leaves ", ""); color(nlv, lime); hidden(nlv);
// ---------- stage ----------
circle(moon, (880, 235), 62); color(moon, #f6e7c0); glow(moon, 20); hidden(moon);
line(gnd, (60, 1668), (1020, 1668)); stroke(gnd, 3); color(gnd, dim); untraced(gnd);
dot(seed, (540, 290), 9); color(seed, gold); glow(seed, 10); hidden(seed);
// ---------- the night sky: 220 twinkling stars ----------
cloud(stars, 220, #ffffff, 0.5) {
let rn = mod(abs(sin(i * 91.17) * 4375.8), 1);
let rn2 = mod(abs(sin(i * 45.7) * 7919.3), 1);
let x = 40 + rn * 1000;
let y = 60 + rn2 * 500;
let tw = 0.5 + 0.5 * sin(2 * t + i * 1.3);
let r = (0.6 + rn * 1.2) * tw * tanh(t) + 0.3;
let hue = 200 + rn * 40;
}
// ---------- the wood: grows out of the planted seed (front starts t~3.6) ----
cloud(wood, 1500, #ffffff, 0.5) {
let p = i / 1500;
let b = mod(i, 5);
let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
let q = 0.5 * ((2 * p - 1) + abs(2 * p - 1));
let pt = 2 * p - q;
let tx = 540 + (b - 2) * 175;
let ty = 800 + abs(b - 2) * 95;
let sx = 540 + 18 * sin(3.1 * pt) + (tx - 540) * q + 12 * sin(3.14159 * q) * (b - 2) * 0.3;
let sy = 1660 - 510 * pt + (ty - 1150) * q;
let wid = 26 * (1 - 0.7 * pt) * (1 - 0.55 * q) + 3;
let jx = (rn - 0.5) * 2 * wid;
let jy = (rn2 - 0.5) * 14;
// wind, with the cold gust that strips the tree near t = 19
let hgt = (1660 - sy) / 900;
let gd = t - 19;
let gust = 1 + 1.3 * exp(-0.4 * gd * gd);
let wind = 14 * sin(0.9 * t + 0.004 * sy) * hgt * hgt * gust;
// growth front: sweeps p = 0..1 starting when the seed has been planted
let sv = tanh((0.3 * (t - 3.6) - p) * 5);
let vfront = 0.5 * (sv + abs(sv));
let x = sx + jx + wind;
let y = sy + jy;
let r = (2.2 + 2.2 * (1 - pt) * (1 - q)) * vfront;
let hue = 22 + rn * 14;
}
// ---------- the leaves: pop, flutter, turn, fall, fade... and RETURN --------
cloud(leaves, 5500, #ffffff, 0.5) {
let b = mod(i, 5);
let k = floor(i / 5);
let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
let tx = 540 + (b - 2) * 175;
let ty = 800 + abs(b - 2) * 95;
let th = k * 2.39996;
let rad = 10.5 * sqrt(mod(k, 210)) + 8 * rn;
let bx = tx + rad * cos(th);
let by = ty + 0.78 * rad * sin(th) - 25;
let px = bx + (540 - bx) * 0.10;
let py = by + (860 - by) * 0.10;
// spring: each leaf unfurls on its own delay (t ~ 6.4 .. 8.8)
let ap0 = tanh((t - 6.4 - 2.4 * rn) * 2.2);
let ap = 0.5 * (ap0 + abs(ap0));
// wind + flutter, gusting near t = 19
let hgt = (1660 - py) / 900;
let gd = t - 19;
let gust = 1 + 1.3 * exp(-0.4 * gd * gd);
let wind = 26 * sin(0.9 * t + 0.004 * py + 2 * rn) * hgt * hgt * gust;
let fl = 4 * sin(2.3 * t + 1.7 * i);
// autumn: green -> gold/red, each leaf at its own pace (t ~ 16 .. 20)
let au0 = (t - 16 - 2.2 * rn) * 0.5;
let au1 = 0.5 * (au0 + abs(au0));
let au = au1 - 0.5 * ((au1 - 1) + abs(au1 - 1));
// the fall: EVERY leaf lets go this time (t ~ 19.5 .. 23), sways down, lands
let s2 = t - 19.5 - 3.5 * rn2;
let dt = 0.5 * (s2 + abs(s2));
let yfree = py + 55 * dt * dt;
let yg = 1665 + 20 * rn;
let yfall = yfree - 0.5 * ((yfree - yg) + abs(yfree - yg));
let sway = 30 * sin(2.2 * dt + i) * tanh(dt) * exp(-0.10 * dt);
// winter: the fallen fade into the soil (t ~ 24.5 .. 27.5)
let go0 = tanh((t - 24.5 - 1.6 * rn) * 1.6);
let gone = 0.5 * (go0 + abs(go0));
// spring again: a NEW leaf opens at the same spot on the branch (t ~ 27 .. 30)
let rb0 = tanh((t - 27 - 2.2 * rn) * 2.0);
let rb = 0.5 * (rb0 + abs(rb0));
// two position tracks, blended: the falling track and the fresh canopy track
let xfall = px + wind * (1 - tanh(2 * dt)) + fl + sway;
let xcan = px + wind + fl;
let x = xfall * (1 - rb) + xcan * rb;
let y = yfall * (1 - rb) + py * rb;
let r = (2.6 + 1.8 * rn2) * (ap * (1 - gone) + rb);
// hue: green, autumn-shifted, reset to green by rebirth
let hgr = 96 + 36 * rn;
let hau = 18 + 34 * rn2;
let hue = hgr + (hau - hgr) * au * (1 - rb);
}
// ---------- the blossoms: open on the young tree, shed petal-snow, return ---
cloud(bloom, 1200, #ffffff, 0.5) {
let b = mod(i, 5);
let k = floor(i / 5);
let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
let tx = 540 + (b - 2) * 175;
let ty = 800 + abs(b - 2) * 95;
let th = k * 2.39996 + 1.3;
let rad = 21 * sqrt(mod(k, 48)) + 6 * rn;
let bx = tx + rad * cos(th);
let by = ty + 0.78 * rad * sin(th) - 25;
let px = bx + (540 - bx) * 0.10;
let py = by + (860 - by) * 0.10;
// first bloom: t ~ 9 .. 11.3
let bp0 = tanh((t - 9 - 1.8 * rn) * 2.4);
let bp = 0.5 * (bp0 + abs(bp0));
// petal-snow: slow drift down from t ~ 12.2, landing softly
let s2 = t - 12.2 - 2.2 * rn2;
let dt = 0.5 * (s2 + abs(s2));
let yfree = py + 16 * dt * dt + 30 * dt;
let yg = 1662 + 22 * rn;
let yfall = yfree - 0.5 * ((yfree - yg) + abs(yfree - yg));
let sway = 40 * sin(1.8 * dt + i) * tanh(dt);
// fallen petals melt away t ~ 18 .. 20.4 (before the leaf carpet arrives)
let go0 = tanh((t - 18 - 1.2 * rn) * 1.8);
let gone = 0.5 * (go0 + abs(go0));
// the second bloom, right at the end: the cycle begins again (t ~ 30.5+)
let rb0 = tanh((t - 30.5 - 1.2 * rn) * 2.6);
let rb = 0.5 * (rb0 + abs(rb0));
let hgt = (1660 - py) / 900;
let wind = 20 * sin(0.9 * t + 0.004 * py + 2 * rn) * hgt * hgt;
let xfall = px + wind * (1 - tanh(2 * dt)) + sway;
let xcan = px + wind + 3 * sin(2.1 * t + i);
let x = xfall * (1 - rb) + xcan * rb;
let y = yfall * (1 - rb) + py * rb;
let tw = 1 + 0.15 * sin(3 * t + i);
let r = (2.2 + 1.6 * rn2) * (bp * (1 - gone) + rb) * tw;
let hue = 318 + 26 * rn;
}
// ================= timeline (narration over the self-evolving year) =========
type(head, 1.1);
par { show(moon, 0.8); draw(gnd, 0.8); }
// ---- the seed
say(cap, "it begins with a single seed", 0.5);
show(seed, 0.3);
shift(seed, (0, 1355), 1.0, in);
cue(pop);
fade(seed, 0.5);
// ---- spring: sprout and first leaves
par { say(cap, "a sprout reaches for the sky", 0.5); say(season, "spring", 0.3); }
wait(2.0);
say(cap, "first leaves unfurl, one by one", 0.5);
show(nlv, 0.3);
to(nlv, value, 5500, 2.6, smooth);
// ---- the flowering
say(cap, "and then - the tree FLOWERS", 0.5);
cue(chime);
wait(1.8);
// ---- summer: petal-snow
par { say(cap, "petals drift away... summer settles in", 0.5); say(season, "summer", 0.3); recolor(season, gold, 0.3); }
wait(3.0);
// ---- autumn
par { say(cap, "autumn arrives, one leaf at a time", 0.5); say(season, "autumn", 0.3); recolor(season, orange, 0.3); }
cue(tick);
wait(2.6);
// ---- the gust: every leaf lets go
say(cap, "a cold wind - and every leaf lets go", 0.5);
cue(whoosh);
to(nlv, value, 0, 4.6, smooth);
// ---- winter
par { say(cap, "winter: the tree remembers in silence", 0.5); say(season, "winter", 0.3); recolor(season, cyan, 0.3); }
wait(2.2);
// ---- spring again
par { say(cap, "...and then, again", 0.5); say(season, "spring, again", 0.3); recolor(season, lime, 0.3); }
cue(whoosh);
to(nlv, value, 5500, 3.0, smooth);
say(cap, "new leaves - and new flowers", 0.5);
cue(chime);
wait(2.6);
// ---- close
say(cap, "a seed, a tree, a year - 8,420 points, one formula each", 0.6);
wait(2.5);
map-attractor
A discrete 2D strange attractor via cloud … from map(...) — the discrete twin of from flow. map
iterates a 2D map (x,y)->(x’,y’) 40,000 times (a stateful recurrence clouds can’t do) and hands each
point the i-th state; the cloud projects it. Covers the Gumowski-Mira / Clifford / de Jong / Hénon
family. @yuruyurau’s idea, hue’d + annotated on a 9:16 Short.
// map-attractor — a discrete 2-D strange attractor, via the new `cloud … from
// map(...)` source: the discrete twin of `from flow`. `map` ITERATES a 2-D map
// (x,y) -> (x',y') 40,000 times (a stateful recurrence clouds alone can't do) and
// hands each point the i-th state as hx/hy; the cloud's formulas project it.
// Covers the whole Gumowski-Mira / Clifford / de Jong / Hénon family. Most maps
// take both formulas from the OLD state (clean); THIS one is sequential
// (y' uses x'), so x' is inlined into y' below.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated,
// bloomed take on a 9:16 Short — pure in (i, t), so it scrubs and records.
//
// manic examples/map-attractor.manic
title("A strange attractor from one map");
canvas("9:16");
template("black");
cloud(atr, 40000, #ffffff, 0.5)
from map("(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))", "-0.8*(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))+3.6*(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))*(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))/(1+(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x))*(y+(1-0.06*y*y)*0.003*y-0.8*x+3.6*x*x/(1+x*x)))-x", (1, 1)) {
// hx, hy = the i-th iterated state; project it through a polar lens
let c = t - hypot(hx, hy)/4;
let px = hy*(5*sin(c) + 11);
let py = hx*(2*cos(c) + 7) + 9*sin(hy/4 + t);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.3 * grow;
let y = 980 + py * 2.3 * grow;
let hue = mod(hypot(hx, hy)*24 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "A strange attractor from one map", (540, 140), 34);
caption(sub, "40,000 iterations, no simulation loop", (540, 208), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `(x,y)\;\to\;(x',\,y')`, 38);
caption(lab, "iterate a discrete map, then project it", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(30);
lorenz-shells
The Lorenz attractor through a different lens, same cloud … from flow(...) bridge (σ=9, ρ=28, β=2):
the integrated state projected via a polar lens fanned into 3 layers into flowing shell forms. One
bridge, endless dynamical-system art. @yuruyurau’s idea, hue’d + annotated.
// lorenz-shells — the Lorenz attractor through a different lens, via the same
// `cloud … from flow(...)` bridge (σ=9, ρ=28, β=2). `flow` integrates the 3D ODE
// and hands each of 30,000 points the i-th state as hx/hy/hz; the cloud projects
// (hx, hz) through a polar lens fanned into 3 layers (mod i,3) into flowing shell
// forms. One bridge, endless dynamical-system art.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated
// take on a 9:16 Short — pure in (i, t), so it scrubs and records.
//
// manic examples/lorenz-shells.manic
title("The Lorenz flow, in shells");
canvas("9:16");
template("black");
cloud(shells, 30000, #ffffff, 0.55)
from flow("9*(y - x)", "x*(28 - z) - y", "x*y - 2*z", (6, 6, 6), 0.001) {
let q = hx*(sin(t*pi/80 - hx*hx/89 + mod(i,3))*0.8 + 1.2)*2 + 99;
let k = hz/39 + t*pi/960 + mod(i,3)*2;
let px = q*sin(k);
let py = q*cos(k);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.2 * grow;
let y = 960 + py * 2.2 * grow;
let hue = mod(mod(i,3)*90 + hz*4 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "The Lorenz flow, in shells", (540, 140), 36);
caption(sub, "integrate chaos, then project it", (540, 208), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `\dot x=\sigma(y{-}x),\;\; \dot y=x(\rho{-}z){-}y,\;\; \dot z=xy-\beta z`, 24);
caption(lab, "same flow as lorenz-attractor, a new lens", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(20);
flow-lorenz
The Lorenz attractor through an artist’s lens, via cloud … from flow(...): flow integrates the
3D Lorenz ODE (a stateful system clouds alone can’t do) and hands each point the i-th state as
hx/hy/hz; the cloud’s formulas project it — here a polar lens fanned into 9 layers. One primitive now
visualises ANY dynamical system through ANY projection. @yuruyurau’s idea, hue’d + annotated.
// flow-lorenz — the Lorenz attractor seen through an artist's lens, via the new
// `cloud … from flow(...)` bridge. `flow` integrates the 3-D Lorenz ODE (a
// STATEFUL system clouds alone can't do); each of 30,000 points receives the
// i-th integrated state as hx/hy/hz, and the cloud's own formulas project that
// state to the screen — here a polar lens fanned into 9 layers (mod(i,9)). So one
// primitive now visualises ANY dynamical system through ANY projection you write.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau); our hue'd, annotated,
// bloomed take on a 9:16 Short. The maths is the real Lorenz flow, not a picture.
//
// manic examples/flow-lorenz.manic
title("A chaotic flow, projected");
canvas("9:16");
template("black");
cloud(art, 30000, #ffffff, 0.55)
from flow("9*(y - x)", "x*(28 - z) - y", "x*y - 2*z", (9, 9, 9), 0.0005) {
// hx, hy, hz = the i-th integrated Lorenz state; project it through the lens
let e = sin(t*pi/20 - hx*hx/99 + mod(i, 9)) + 1;
let q = hx*e + 89;
let k = hz/59 - e/29 + t*pi/480 + mod(i, 9)*8;
let px = q*cos(k);
let py = (q + 60*cos(k/2)) * sin(k);
let grow = tanh(t*0.5 + 0.12); // bloom from the centre
let x = 540 + px * 2.2 * grow;
let y = 960 - py * 2.2 * grow;
let hue = mod(mod(i, 9)*40 + hz*3 + t*14, 360); // colour by layer + depth
}
// ---- textbook annotations ----
caption(head, "A chaotic flow, projected", (540, 140), 36);
caption(sub, "integrate the Lorenz system, then project it", (540, 208), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706),
`\dot x=\sigma(y{-}x),\;\; \dot y=x(\rho{-}z){-}y,\;\; \dot z=xy-\beta z`, 24);
caption(lab, "the state → your own lens, per point", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-phoenix
Another @yuruyurau art-tweet in ONE cloud: 10,000 points flow into a living, wing-like form and
morph over time. The original packs a (y<9?9:5) ternary into the amplitude; manic formulas have no
comparisons, so it’s written with sign(). Hue-gradient coloured and bloomed on a 9:16 Short, pure
in (i, t) so it scrubs — the p5 original can’t.
// cloud-phoenix — another @yuruyurau art-tweet, reimagined in ONE `cloud`. 10,000
// points flow into a living, wing-like form and morph over time. The original
// packs a conditional into the amplitude ((y<9?9:5)); manic formulas have no
// ternary, so it's written with sign(): amp = 7 + 2*sign(9 - s). Coloured per
// point and bloomed from the centre on a 9:16 Short — every point a pure function
// of (i, t), so it scrubs and records (the p5 original can't).
//
// Original idea by @yuruyurau (https://x.com/yuruyurau) — a prolific poster of
// these tiny p5.js/dwitter art formulas. This is our own hue'd, annotated take.
//
// manic examples/cloud-phoenix.manic
title("One formula becomes a phoenix");
canvas("9:16");
template("black");
cloud(bird, 10000, #ffffff, 0.72) {
let s = i / 353; // the reference's "y" parameter
let amp = 7 + 2*sign(9 - s); // (y<9?9:5), written with sign()
let k = (amp + cos(s*31 - t)) * cos(i/44);
let e = s/9 - 14;
let d = hypot(k, e) / 1.6;
let c = d - t/2;
// raw coords (centred at 0), then bloom + scale onto the 1080x1920 frame
let px = (d*9 + k*k)*cos(c);
let py = (55 + d*9)*sin(c/3) + 4*sin(k*2) + s/29*k*(e + 3*sin(e*4 - d*4 + t*3));
let grow = tanh(t * 0.5 + 0.12);
let x = 540 + px * 2.6 * grow;
let y = 960 + (py - 120) * 2.6 * grow; // -120 recentres the form's DC offset
let r = 1.4;
let hue = mod(s * 13 + t * 16, 360); // a rainbow flowing along the form
}
// ---- textbook annotations ----
caption(head, "One formula becomes a phoenix", (540, 132), 38);
caption(sub, "10,000 points, no simulation", (540, 200), 24);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p_i = f(i,\; t)`, 46);
caption(lab, "each point placed by its index i and time t", (540, 1786), 22);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(27);
cloud-pottery
The OpenProcessing weekly ‘Pottery’ challenge in ONE cloud: seven stippled vases, each a UNIQUE
wavy silhouette from a couple of seeded sine harmonics, drawn as dotted ellipse-rings shaded
front-bright by per-dot alpha, with flowering sprigs on a hash-picked subset (a curved stem + a
dotted 5-lobe head). Every dot is a pure function of index and time, so the pots GROW in bottom-up
(tanh(t)), the sprigs sprout, and then the glaze CYCLES through the colour ‘sets’ the p5 original
picks at random — same scene, live, and it scrubs/records exactly. The recursive branching stays with
the original (a flat f(i,t) cloud can’t recurse); the signature look is one formula per point.
// cloud-pottery — the OpenProcessing weekly "Pottery" creative-coding challenge
// (https://openprocessing.org/curation/78544) reimagined in ONE manic `cloud`. The p5
// original stacks random easing curves into pot silhouettes and stipples them with
// thousands of dots across three graphics layers, plus recursive sticks + flowers.
// manic can't recurse in a flat `f(i,t)` cloud — so the sticks/flowers/layers are left
// to the original — but the SIGNATURE look (dotted ellipse-ring vases, each a unique
// wavy profile, shaded front-bright) is one closed-form formula per point. Pure in
// (i,t): the pots grow in bottom-up and record exactly, where the p5 sketch draws once.
//
// manic examples/cloud-pottery.manic
title("Pottery — a p5 sketch, reimagined as one cloud");
canvas("16:9");
template("black");
cloud(pots, 40320, #c47a3d, 0.95) {
// decode the flat index i into (pot, ring, angle-around-the-ellipse)
let per = 5760; // 90 rings * 64 dots
let pot = floor(i / per);
let rem = i - pot * per;
let ring = floor(rem / 64);
let k = rem - ring * 64;
let yl = ring / 89; // 0 = base, 1 = rim
let a = k / 64 * tau;
// 7 pots across a 1280-wide frame (margin 86)
let span = 1108;
let potX = 86 + (pot + 0.5) * (span / 7);
let potW = (span / 7) * 0.4; // half-width
let baseY = 545;
let potH = potW * (1.0 + 1.8 * rand2(pot, 1.7));
// a wavy vase silhouette, unique per pot (a couple of seeded sine harmonics)
let seed = rand2(pot, 3.7);
let prof = 0.55 + 0.28 * sin(yl * pi + seed * tau) + 0.12 * sin(yl * tau + seed * 9.0);
let R = potW * prof;
// ellipse ring, 0.22 vertical squash for perspective, a little per-dot jitter
let x = potX + R * sin(a) + 1.4 * rand2(i, 2.1);
let y = baseY - yl * potH + R * 0.22 * (-cos(a)) + 1.4 * rand2(i, 5.3);
// grow bottom-up: only rings below the rising build-line have appeared
let build = tanh(t * 0.55) * 1.14;
let vis = 1 - smoothstep(build, build + 0.10, yl);
let r = 0.9 + 1.1 * rand2(i, 7.0); // fine stipple
// terracotta while it grows; AFTER the pots are formed the glaze shifts, cycling
// through the "colour sets" the p5 original picks at random — same scene, live.
let cyc = (t - 4.0) * step(4.0, t); // 0 until t=4, then climbs
let hue = mod(26 + cyc * 34.0 + 16 * rand2(pot, 4.0), 360);
let alpha = (0.38 + 0.55 * (0.5 - 0.5 * cos(a))) * vis; // near-side brighter; fades in
}
// flowering sprigs on SOME vessels (the original branches recursively; a flat cloud
// can't, so this is a hash-selected subset with a curved stem + a dotted flower head)
cloud(sprigs, 12000, #ffdd88, 0.92) {
let per = 2000;
let pot = floor(i / per);
let rem = i - pot * per;
let has = step(0.55, rand2(pot, 9.1)); // ~45% of pots get a sprig
// recompute the pot geometry so the sprig sits on the rim
let span = 1108;
let potX = 86 + (pot + 0.5) * (span / 7);
let potW = (span / 7) * 0.4;
let baseY = 545;
let potH = potW * (1.0 + 1.8 * rand2(pot, 1.7));
let rimY = baseY - potH;
let seedF = rand2(pot, 5.9);
let stemLen = potH * (0.8 + 0.7 * seedF);
let lean = (rand2(pot, 2.3) - 0.5) * 80;
let isStem = 1 - step(600, rem); // first 600 dots = stem, rest = head
// stem: a gentle bow from the rim upward
let ts = rem / 600;
let sx = potX + lean * ts + 10 * sin(ts * pi + seedF * 6.0);
let sy = rimY - stemLen * ts;
// flower head at the stem top (ts = 1)
let hx = potX + lean + 10 * sin(pi + seedF * 6.0);
let hy = rimY - stemLen;
let tf = (rem - 600) / 1400;
let fang = tf * tau * 7.0; // spiral fills the head
let petal = 0.5 + 0.5 * abs(sin(fang * 2.5)); // 5-lobe petals
let frad = (14 + 8 * seedF) * petal * (0.35 + 0.65 * rand2(i, 3.3));
let fx = hx + frad * sin(fang) + 3 * (rand2(i, 7.7) - 0.5);
let fy = hy + frad * (-cos(fang)) + 3 * (rand2(i, 8.8) - 0.5);
let x = select(sx, fx, isStem);
let y = select(sy, fy, isStem);
let r = select(1.0 + 0.5 * rand2(i, 1.1), 1.3 + 1.5 * rand2(i, 2.2), isStem);
let hue = select(96, 44, isStem); // stem green, flower gold
let grow = smoothstep(2.6, 4.6, t); // sprigs sprout after the pots form
let alpha = has * grow * (0.4 + 0.55 * rand2(i, 6.6));
}
// ---- textbook annotations ----
caption(head, "Pottery — one formula per dot", (640, 60), 30);
caption(sub, "vases grown bottom-up, sprigged, then re-glazed live — all pure in (i, t)", (640, 112), 20);
hidden(head);
hidden(sub);
show(head);
wait(2.4);
show(sub);
wait(11); // pots grow, sprigs sprout, then the glaze cycles the palette
cloud-kaleidoscope
A 14-fold kaleidoscope mandala in ONE cloud. The @yuruyurau original uses canvas feedback
(get()+rotate+image() — a raster trick manic doesn’t have); the OUTCOME is 14-fold rotational
symmetry, which cloud gets by layer-replication (one base field copied at 14 angles). Same picture,
but deterministic — it scrubs and records. Hue per sector, on a 9:16 Short.
// cloud-kaleidoscope — a 14-fold kaleidoscope in ONE `cloud`. The @yuruyurau
// original uses canvas FEEDBACK (get() + rotate + image()) — a raster/Droste
// trick manic doesn't have (it's vector & deterministic). But the OUTCOME is a
// 14-fold rotational symmetry, which `cloud` gets by layer-replication: one base
// field, copied at 14 angles via a layer index `L = floor(i/bn)` and rotated by
// `L·π/7`. So the picture is the same, but it scrubs and records (the p5 can't).
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated take.
//
// manic examples/cloud-kaleidoscope.manic
title("A kaleidoscope from one formula");
canvas("9:16");
template("black");
cloud(kaleido, 56000, #ffffff, 0.6) {
let bn = 4000; // points per copy (14 copies = 56k, smooth)
let L = floor(i / bn); // copy 0..13
let j = mod(i, bn); // base index
let k = mod(j, 50) - 25;
let e = j/222;
let d = 5*cos(hypot(k, e) - t + mod(j, 2));
let bx = k + k*d/6*sin(d + e/3 + t);
let by = 90 + e*d - e/d*2*cos(d + t);
let ang = L * pi/7; // 14-fold rotation of the base field
let grow = tanh(t*0.5 + 0.12);
let x = 540 + (bx*cos(ang) - by*sin(ang)) * 2.2 * grow;
let y = 960 + (bx*sin(ang) + by*cos(ang)) * 2.2 * grow;
let r = 0.8;
let hue = mod(L*26 + by*2 + t*12, 360); // a colour per sector + radial
}
// ---- textbook annotations ----
caption(head, "A kaleidoscope from one formula", (540, 138), 34);
caption(sub, "one field, copied at 14 angles", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `\vec p_L = R\!\left(L\tfrac{2\pi}{14}\right)\,\vec p_0`, 34);
caption(lab, "14-fold symmetry, no mirrors — pure rotation", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-medusae
Another @yuruyurau art-tweet in ONE cloud: 30,000 points in interleaved layers (mod(i,5/4/2))
swirl into a ring of tailed medusae. A polar plot; two JS bitwise-isms translated (**4->^4,
~(i&1)*80->(1+mod(i,2))*80). Hue-coloured, bloomed, 9:16.
// cloud-medusae — another @yuruyurau art-tweet in ONE `cloud`: 30,000 points in
// interleaved layers (`mod(i,5)`, `mod(i,4)`, `mod(i,2)`) swirl into a ring of
// tailed medusae that pulse over time. A polar plot (radius `q`, angle `c`).
// Two JS bitwise-isms translated: `**4` → `^4`, and `~(i&1)*80` (bitwise NOT of
// i&1) → `(1+mod(i,2))*80` — an 80/160 offset per parity. Hue'd + bloomed, 9:16.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our own annotated take —
// pure in (i, t), so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-medusae.manic
title("A ring of medusae from one formula");
canvas("9:16");
template("black");
cloud(medusae, 30000, #ffffff, 0.55) {
let s = i/799;
let k = 5*cos(i/48);
let e = 5*cos(s/9);
let d = (hypot(k, e)/(6 + mod(i,4)))^4 + 4;
let q = k*(3 + e/2*sin(d*8 + k/9 - t)) - 3*sin(k*d/3) + (1 + mod(i,2))*80;
let c = d - t/9 + mod(i,5);
let px = q*sin(c);
let py = q*cos(c - mod(i,2) + mod(i,5)*3 + 7);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 1.7 * grow;
let y = 960 + py * 1.7 * grow;
let hue = mod(mod(i,5)*72 + i*0.01 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "A ring of medusae from one formula", (540, 138), 32);
caption(sub, "30,000 points, no simulation", (540, 204), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; q\cos c')`, 32);
caption(lab, "a polar plot in interleaved layers (mod i,n)", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-koi
Another @yuruyurau art-tweet in ONE cloud: 10,000 points in two layers (mod(i,2)) swirl into
koi-like forms chasing each other. A polar plot with raw-index texture (cos(i+t/4)); the p5
y^9 (bitwise XOR — no manic operator) is approximated with noise(). Hue-coloured, bloomed, 9:16.
// cloud-koi — another @yuruyurau art-tweet in ONE `cloud`: 10,000 points in two
// layers (`mod(i,2)`) swirl into koi-like forms chasing each other, rippling over
// time. A polar plot (radius `q`, angle `c`); `cos(i+t/4)` on the raw index gives
// the fine scale texture. One JS-ism: the original's `y^9` is bitwise XOR (a
// per-band scramble), which manic has no operator for — approximated here with
// `noise()`, visually equivalent. `mag` = `hypot`; parameter renamed `s`.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated
// take — pure in (i, t), so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-koi.manic
title("Two koi from one formula");
canvas("9:16");
template("black");
cloud(koi, 10000, #ffffff, 0.6) {
let s = i/790;
let m = mod(i, 2);
let sw = 0.5*(1 + sign(8 - s)); // (y<8 ? … : …)
let scr = noise(s*2, 0); // ~ the JS y^9 XOR scramble
let kbase = sw*(9 + scr*6) + (1 - sw)*(4 + cos(s));
let k = kbase * cos(i + t/4);
let e = s/3 - 13;
let d = hypot(k, e) + cos(e + t*2 + m*4);
let q = s*k/5*(2 + sin(d*2 + s - t*4)) + 80;
let c = d/4 - t/2 + m*3;
let px = q*cos(c);
let py = q*sin(c) + d*9 - 130; // recentre the d*9+60 offset
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.1 * grow;
let y = 960 + py * 2.1 * grow;
let hue = mod(m*90 + i*0.03 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "Two koi from one formula", (540, 138), 36);
caption(sub, "10,000 points, no simulation", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\cos c,\;\; q\sin c)`, 32);
caption(lab, "a polar plot: radius q, angle c, per point", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-swifts
Another @yuruyurau creature in ONE cloud: two lace birds turning around a shared centre, and a
staging built to expose the two terms that matter. (i%2)*8 is the ENTIRE reason there are two
animals, so the parity that splits them also colours them — amber is mod(i,2)=0, cyan is 1.
And x takes cos c while y takes sin(c/3): a 3:1 figure, not a circle, which is why they bank and
cross instead of chasing round a ring. The sky is the same primitive — an inclined spiral galaxy is
two clouds more (a two-arm sweep and a twinkling starfield), so nothing here is a background image
and nothing is a simulation. Full size on the first frame, because that frame is the preview. 9:16.
// cloud-swifts — another @yuruyurau creature in ONE `cloud`: two lace birds, forked
// tails and trailing streamers, turning around a shared centre. The reference is a
// tweet-sized golf:
// y = i/663, k = (4 + cos y)·cos i, e = y/5 - 11, d = mag(k,e) - 5
// c = d/2.5 - t/2 + (i%2)·8
// point( (79 + k²)·cos c + 200 ,
// 99·sin(c/3) + 200 + d²·sin(2t - d) + 3·sin 2k + sin(y/9+6)·k·(e + sin(4e - 4d)) )
//
// Two things in there do all the work, and this staging is built to make both VISIBLE:
//
// · `(i%2)·8` — every other point has its angle shifted by 8 radians, so ONE formula
// draws TWO animals. Nothing else separates them. So we colour by that same parity:
// amber is `mod(i,2) = 0`, cyan is `1`. The two birds differ by one term, and now
// you can see which one.
// · `cos c` across, `sin(c/3)` down — the vertical angle runs at a THIRD the rate of
// the horizontal one. A 3:1 Lissajous, not a circle: that is why they lean into the
// turn and cross over instead of chasing each other round a ring, and why each body
// is lobed rather than a smooth arc.
//
// The rest is texture. `cos i` on the raw index — not on `y` — is what frets the wings
// into lace: neighbouring points land far apart, so the body fills as a woven mesh
// rather than a line. `d²·sin(2t - d)` is the flap. And alpha rides the index, so each
// bird is dense through the body and dries out along the streamers.
//
// The sky they cross is the SAME primitive, which is the other half of the argument:
// an inclined spiral galaxy is two `cloud`s more — an arm sweep and a starfield — so
// nothing here is a background image, and nothing is a simulation. Both are arithmetic
// on a point index, and both are pure in `(i, t)`, so the whole frame scrubs.
//
// Nothing fades up: at t = 0 the birds are already at full size over a finished sky,
// because the first frame is the one a feed shows as the preview.
//
// Faithful notes: p5's `mag` is `hypot` and `%` is `mod`; the parameter is renamed `s`
// because `y` is an output here. The p5 original chains its `let`s through default
// parameter values (`(y, d = mag(k=…, e=…) - 5) =>`) — a golfing trick for statements
// that were always just `let`s, so they are written as `let`s. p5's draw loop advances
// `t` by PI/80 per FRAME; ours is in seconds, which runs the turn slower on purpose —
// the crossing is the thing to watch. Pure in (i, t), so it scrubs, seeks and records
// exactly, which the p5 original cannot do.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our own annotated take.
//
// manic examples/cloud-swifts.manic
title("Two swifts from one formula");
canvas("9:16");
template("black");
bloom(0.32, 0.6, 24);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// ---- the sky: an inclined spiral, core high and right so the birds cross an arm -----
cloud(galaxy, 30000, #ffffff, 1.0) {
let u = i/30000; // 0 at the core, 1 at the rim
let arm = mod(i, 2); // two arms, half a turn apart
let ha = cos(i*1.618); // two deterministic scatters, so the
let hb = sin(i*2.399); // arms have WIDTH, not just a curve
let th = u*7.4 + arm*3.14159 // 7.4 rad — about 1.2 turns of arm
+ ha*0.34*(1 - u*0.5) // fuzz, tighter near the core
+ t*0.012; // one slow turn, barely there
let rad = 60 + 560*u + hb*(46 + 150*u); // the arm widens as it goes out
let ax = rad*cos(th);
let ay = rad*sin(th)*0.46 + ha*16; // squashed: a disc seen well off-axis
let x = 700 + ax*0.848 + ay*0.530; // …and tilted 32°, so the arms sweep
let y = 520 - ax*0.530 + ay*0.848; // down across the birds' path
let hue = mod(32 + u*198, 360); // warm core, blue arms
let sat = 0.34 + u*0.5;
let val = 1 - u*0.28;
let r = 0.85 + hb*0.4;
let alpha = 0.42 - u*0.36; // fades to nothing: no rim to see
}
cloud(bulge, 3400, #ffffff, 1.0) {
let u = i/3400;
let rad = 128*u^0.55; // ~even packing at the golden angle
let th = i*2.39996;
let ax = rad*cos(th);
let ay = rad*sin(th)*0.62;
let x = 700 + ax*0.848 + ay*0.530; // same centre and tilt as the disc
let y = 520 - ax*0.530 + ay*0.848;
let hue = mod(42 - u*10, 360);
let sat = 0.16 + u*0.30;
let r = 0.95;
let alpha = 0.52 - u*0.36; // bloom does the rest
}
cloud(stars, 2600, #ffffff, 1.0) {
let hx = cos(i*2.399)*0.5 + 0.5; // two hashes, uncorrelated enough
let hy = sin(i*1.732)*0.5 + 0.5;
let x = 40 + hx*1000;
let y = 40 + hy*1840;
let r = 0.7 + cos(i*5.1)*0.5;
let hue = mod(200 + cos(i*3.3)*40, 360);
let sat = 0.22;
let alpha = 0.30 + 0.22*sin(t*1.7 + i); // twinkle
}
// ---- the birds ---------------------------------------------------------------------
cloud(swifts, 20000, #ffffff, 0.55) {
let s = i/663;
let m = mod(i, 2); // the parity that makes it two birds
let k = (4 + cos(s)) * cos(i); // cos of the raw index — the lace
let e = s/5 - 11;
let d = hypot(k, e) - 5;
let c = d/2.5 - t/2 + m*8; // …shifted 8 rad for the second bird
let px = (79 + k*k) * cos(c);
let py = 99*sin(c/3) // a THIRD the rate: 3:1, so they bank
+ d*d*sin(t*2 - d) // the flap
+ 3*sin(k*2)
+ sin(s/9 + 6) * k * (e + sin(e*4 - d*4));// the streamers
let x = 540 + px * 4.1;
let y = 960 + py * 4.1;
let hue = mod(35 + m*161 + sin(t/5)*7, 360); // amber / cyan, by that same parity
let alpha = 0.26 + s*0.016; // dense body, dry streamers
}
// ---- textbook annotations ----
caption(head, "Two swifts from one formula", (540, 138), 36);
caption(sub, "20,000 points, no simulation", (540, 206), 22);
hidden(sub);
equation(eq, (540, 1666), `p = \left((79+k^2)\cos c,\;\; 99\sin\tfrac{c}{3}\right)`, 30);
caption(lab, "down turns at a third of across — a 3:1 figure, so they bank", (540, 1744), 20);
caption(par, "amber and cyan differ by one term: mod(i, 2) x 8", (540, 1792), 20);
hidden(eq);
hidden(lab);
hidden(par);
wait(1.6);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(1.2);
show(par);
wait(24);
cycloid-cyclogon
Four shapes roll along a line, twice, each carrying a marked point: the circle’s draws a CYCLOID,
a polygon’s draws a CYCLOGON. Matched perimeters mean all four travel two arches and hit every cusp
together — and let one camera track all four. A polygon does not roll, it PIVOTS, and here that is
literal: each tip is a real turn about the actual contact corner with its arc drawn alongside at the
same duration and ease, so the arc’s tip IS the marked point. It pivots about EVERY vertex including
the marked one — that pivot draws nothing (radius zero) but it is where the cusp comes from. The
construction never asks where the point is, only its distance from each pivot, so the triangle carries
two: gold on a corner touching down each arch, mint at the centroid tracing scallops that never reach
the line. Regular against irregular is the bottom: the square’s arcs all sweep 90° with only the radius
changing; the convex quadrilateral’s sweep 60.3°, 108.0° and 84.2°, so its arch is visibly lopsided —
and still lands at one perimeter, because any convex polygon’s exterior angles sum to a full turn.
// 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);
roulettes
The whole family in one frame, under one rule: roll a shape along a track without slipping, mark a
point rigidly attached to it, and watch where the point goes. Nothing changes between the five rows
but the ROLLER and where the PEN sits — circle on the rim gives a CYCLOID, circle inside and outside
give the curtate and prolate TROCHOIDS (three pens on one wheel, so all three nest on the same row),
polygon at a vertex gives a CYCLOGON, parabola at the focus gives a CATENARY, and a LINE rolling on a
circle gives its INVOLUTE — the same rule turned inside out. All five land together, by arithmetic
rather than fudging: two cycloid arches are 904.7787 px, so the hexagon’s side is that over twelve and
twelve 60° pivots carry it exactly two perimeters, and the catenary’s scale is chosen so its rolled
arc comes to the same number. The top three rows are not drawn but ROLLED — real roll calls, real
turns about the actual contact corners, and every coloured curve a trail of where its pen has
been. No parametrisation is typed anywhere in them; the curve is the residue of the motion.
// 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);
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. The reason they belong together is that every one of them is z(t) = A·e^(iat) + B·e^(ibt) — two arms, one turning on a hub and the second turning on the end of the first. Rolling OUTSIDE makes both arms turn the same way; rolling INSIDE reverses the second, and that sign is the entire difference between epi and hypo. The hero is one wheel with THREE pens, so epitrochoid and EPICYCLOID are a distance apart and not different curves: d < r stays clear, d = r reaches the fixed circle exactly (that is the cusp, and the epicycloid), d > r swings past and loops. Below are the three cases you get by tuning the arms instead of the wheel — flip the sign for a deltoid; equal SPEEDS (R = 2r) makes two counter-rotating arms add to an ellipse with semi-axes A+B and |A−B|, verified to 7e-16, collapsing to the Tusi couple’s exact straight line at d = r; equal RADII (d = R−r) factors the sum into a rose ρ = 2A·cos(nφ), checked to 4e-14. It runs two laps, which is what it takes to close everything: three of the curves are drawn twice over, and the rose — b/a = −1/2, so its arms only come home together after two — exactly once, on the last frame.
// 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);
caustic-family
One mirror, one lamp, and a knob. Slide the lamp away from a circular mirror and the bright curve inside it walks continuously from a CARDIOID to a NEPHROID — and the LIMAÇON underneath explains why. Reflect the lamp in the tangent at every point of the circle and that locus (the orthotomic) is exactly a limaçon, ρ = |2 − 2s·cos t| about the lamp, which is ρ = b + a·cos θ with b = 2 and a = −2s — an identity, residual 2e-15, not a fit. One number, the lamp’s distance s in mirror radii, walks it through every shape it has, and s = 1 is the hinge where b = |a| and the limaçon IS a cardioid. The caustic is the EVOLUTE of that limaçon: computed both ways and compared, the evolute and the ray envelope agree to the sampling spacing. Both ends are checked, not quoted — lamp on the rim gives ρ = ⅔(1 + cos φ) about (−⅓, 0) to 3e-13, which is the SAME cardioid examples/cardioid.manic gets from the two times table, so chords on a circle and light in a mirror land on the identical curve; lamp at infinity gives (x²+y²−4a²)³ = 108a⁴y² to 3e-08. Nothing draws a caustic: every ray is d’ = d − 2(d·n)n, and a ray only counts if it arrives heading outward, which is what reflecting off the inside of a cup means — so the light draws exactly half the nephroid and the drawn curve shows which half.
// caustic-family — one mirror, one lamp, and a knob. Slide the lamp away from a circular
// mirror and the bright curve inside it walks continuously from a CARDIOID to a NEPHROID —
// and the LIMAÇON is the thing underneath that explains why.
//
// Reflect the lamp in the tangent line at every point of the circle. That locus has a name,
// the orthotomic, and it is exactly a limaçon:
//
// S' = S - 2(s·cos t - 1)·P ⇒ ρ = |2 - 2s·cos t| about the lamp
//
// which is ρ = b + a·cos θ with b = 2 and a = -2s. Checked at s = 0, 0.5, 1, 2 and 3.7 —
// residual 2e-15, so this is an identity and not a fit. ONE number, the lamp's distance s in
// units of the mirror's radius, walks that limaçon through every shape it has, and s = 1 is
// the hinge where b = |a| and the limaçon IS a cardioid. That is the row along the bottom.
//
// The caustic — the bright curve, where reflected rays actually crowd — is the EVOLUTE of
// that limaçon. Computed both ways here and compared: the evolute of the orthotomic and the
// envelope of the reflected rays agree to 4e-03, which is the spacing of the sampled clouds
// rather than a disagreement.
//
// So the two ends of the slide are two famous curves, and both are checked rather than
// quoted:
//
// lamp ON the mirror (s = 1) caustic = ρ = ⅔(1 + cos φ) about (-⅓, 0) to 3e-13
// lamp at INFINITY (s → ∞) caustic = (x²+y²-4a²)³ = 108a⁴y², a = ¼ to 3e-08
//
// The first is a CARDIOID, and not merely a cardioid — it is the SAME one that
// examples/cardioid.manic gets from the two times table, ⅔(1 + cos φ) about a centre one
// third of the way back. Chords on a circle and light in a mirror land on the identical
// curve. The second is the NEPHROID of examples/nephroid.manic, cusps at ±R/2 on the axis
// and horns out on the rim.
//
// Nothing here draws a caustic. Every ray is worked out properly — d' = d - 2(d·n)n with n
// the outward normal at the point of contact — and the curve is what appears where the rays
// pile up. The cardioid at the start and the nephroid at the end are drawn independently, so
// the envelope lands on them rather than being them.
//
// It is a mirror and not a tangent-line exercise: a ray only counts if it arrives at its
// point heading OUTWARD, d·n > 0, which is what reflecting off the inside of a cup means.
// With the lamp on the rim every chord qualifies and the cardioid comes out whole. As the
// lamp recedes only the far wall is lit, so the light draws exactly HALF the nephroid — the
// other half of the drawn curve is there to show you which half it made.
//
// The lamp leaves the frame long before the sweep ends; when the incoming rays go parallel,
// that is what s → ∞ looks like from inside the picture.
//
// manic examples/caustic-family.manic
title("Slide the lamp: a cardioid becomes a nephroid");
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, "Slide the lamp: a cardioid becomes a nephroid", (540, 116), 26);
// ---- the mirror -------------------------------------------------------------------
circle(mirror, (540, 600), 330);
outlined(mirror); outline(mirror, dim); stroke(mirror, 3);
// ---- the light arriving, clipped so it stays in frame ------------------------------
cloud(incoming, 4800, #ffffff, 1.0) {
let per = 30;
let c = (i - mod(i, per))/per; // which ray, 0..159
let v = mod(i, per)/29;
let th = 6.283185*(c + 0.5)/160;
let px = 540 + 330*cos(th);
let py = 600 + 330*sin(th);
let s = 1/(1 - 0.93*min(t/14.0, 1)); // the lamp, receding to infinity
let dx0 = px - (540 + 330*s);
let dy0 = py - 600;
let len = max(sqrt(dx0*dx0 + dy0*dy0), 1);
let dx = dx0/len;
let dy = dy0/len;
let back = min(len, 400);
let x = px + (v - 1)*back*dx;
let y = py + (v - 1)*back*dy;
// a ray only reflects off the INSIDE if it arrives at P heading outward, d·n > 0.
// Gating on that is what makes this a mirror rather than a tangent-line exercise.
let dot = dx*cos(th) + dy*sin(th);
let hue = 46;
let sat = 0.3;
let r = 1.1;
let alpha = 0.15*min(max(dot*30, 0), 1);
}
// ---- and reflecting: d' = d - 2(d·n)n, worked out per ray --------------------------
cloud(reflected, 8960, #ffffff, 1.0) {
let per = 56;
let c = (i - mod(i, per))/per;
let v = mod(i, per)/55;
let th = 6.283185*(c + 0.5)/160;
let px = 540 + 330*cos(th);
let py = 600 + 330*sin(th);
let s = 1/(1 - 0.93*min(t/14.0, 1));
let dx0 = px - (540 + 330*s);
let dy0 = py - 600;
let len = max(sqrt(dx0*dx0 + dy0*dy0), 1);
let dx = dx0/len;
let dy = dy0/len;
let dot = dx*cos(th) + dy*sin(th);
let rx = dx - 2*dot*cos(th); // the reflection law, once
let ry = dy - 2*dot*sin(th);
let x = px + v*600*rx;
let y = py + v*600*ry;
let hue = mod(24 + c*1.5, 360);
let sat = 0.72;
let r = 1.1;
let alpha = 0.36*min(max(dot*30, 0), 1)*(1 - 0.45*v);
}
// ---- the lamp, while it is still in the frame --------------------------------------
cloud(lamp, 120, #ffffff, 1.0) {
let s = 1/(1 - 0.93*min(t/14.0, 1));
let a = i/120*6.283185;
let rr = 9*(i/120);
let x = 540 + 330*s + rr*cos(a*9);
let y = 600 + rr*sin(a*9);
let hue = 48;
let sat = 0.5;
let r = 2;
let alpha = min(max(2.4 - s, 0), 1);
}
// ---- the two ends, drawn independently for the envelope to land on -----------------
polar(card, (430, 600), 330, 330, "0.66667*(1 + cos(t))");
color(card, gold); stroke(card, 4); opacity(card, 0.6);
param(neph, (540, 600), 330, 330,
"0.75*cos(t) - 0.25*cos(3*t)", "0.75*sin(t) - 0.25*sin(3*t)", (0, 6.283185));
color(neph, cyan); stroke(neph, 4); opacity(neph, 0.6); untraced(neph);
// ---- the orthotomic: reflect the lamp in every tangent -> a LIMAÇON ----------------
// s = 1 is the hinge, where b = |a| and the limaçon is a cardioid
circle(m5, (240.9, 1330), 81.7);
outlined(m5); outline(m5, dim); stroke(m5, 2); opacity(m5, 0.45);
circle(l5, (281.7, 1330), 7); color(l5, fg); opacity(l5, 0.8);
param(o5, (240.9, 1330), 81.7494, 81.7494,
"0.5 - 2*(0.5*cos(t) - 1)*cos(t)", "0 - 2*(0.5*cos(t) - 1)*sin(t)", (0, 6.283185));
color(o5, mint); stroke(o5, 3); untraced(o5);
circle(m10, (592.0, 1330), 69.3);
outlined(m10); outline(m10, dim); stroke(m10, 2); opacity(m10, 0.45);
circle(l10, (661.2, 1330), 7); color(l10, fg); opacity(l10, 0.8);
param(o10, (592.0, 1330), 69.2814, 69.2814,
"1.0 - 2*(1.0*cos(t) - 1)*cos(t)", "0 - 2*(1.0*cos(t) - 1)*sin(t)", (0, 6.283185));
color(o10, gold); stroke(o10, 3); untraced(o10);
circle(m20, (924.7, 1330), 51.1);
outlined(m20); outline(m20, dim); stroke(m20, 2); opacity(m20, 0.45);
circle(l20, (1027.0, 1330), 7); color(l20, fg); opacity(l20, 0.8);
param(o20, (924.7, 1330), 51.1313, 51.1313,
"2.0 - 2*(2.0*cos(t) - 1)*cos(t)", "0 - 2*(2.0*cos(t) - 1)*sin(t)", (0, 6.283185));
color(o20, magenta); stroke(o20, 3); untraced(o20);
// ---- the identity the whole picture rests on --------------------------------------
equation(eq, (540, 1700),
`S'=S-2(s\cos t-1)P\;\Longrightarrow\;\rho=|2-2s\cos t|`, 26);
par {
draw(o5, 1.6);
draw(o10, 1.6);
draw(o20, 1.6);
seq { wait(1.6); fade(card, 1.6); } // the lamp has left s = 1 by now
seq { wait(11.4); draw(neph, 2.4); } // …and by here the rays are all but parallel
}
wait(3);
hypocycloid-double
The inside family, and Bernoulli’s theorem that says you have been counting the wheels wrong. Roll a circle of radius r inside a fixed circle R with the pen on its rim and you get a hypocycloid with n = R/r cusps — n = 2 the TUSI COUPLE (a straight line, |y| exactly 0), n = 3 the DELTOID, n = 4 the ASTROID (x = R·cos³θ, residual 6e-16). Lift the pen off the rim and the cusps round off into a HYPOTROCHOID. The theorem: a wheel of radius r and a wheel of radius R−r, rolling inside the SAME circle, trace exactly the same curve — not approximately. Sampled independently, the largest gap between the two falls off exactly as 1/N, 7.854e-04 at four thousand points and 7.854e-06 at four hundred thousand, which is sample spacing and nothing else. So the astroid is drawn twice at once by two wheels that look nothing alike: R/4 shuts after one lap, 3R/4 nearly fills the circle it rolls in and needs THREE laps to lay down the same four cusps. Both start at the same cusp and finish together, one thin line lying inside the other. Every n pairs off this way — and n = 2 pairs with itself, because R−r = r when R = 2r, making the Tusi couple the fixed point of the pairing. The pairing needs the pen on the rim, which is why the hypotrochoid panel has one wheel and means it.
// 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 = R/r 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 R/4 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: R/4, 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 = R/r \ \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);
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, and the material point there moves at |i e^(iφ) − i e^(−iφ/k) e^(2πim/k)|, which is zero only at isolated phases. The reason the nesting works is not geometry but SU(k). A trace depends only on eigenvalues and every SU(k) matrix is conjugate to a diagonal one, so the set of traces is exactly {e^(iθ₁)+…+e^(iθₖ) : Σθ ≡ 0} — and that set is precisely the filled k-cusp hypocycloid. The bottom row is not a drawing of that claim: each panel is thousands of ACTUAL traces, swept low-discrepancy over the angles, filling to the boundary drawn over them. SU(2) traces are 2·cos θ, real with |Im| exactly 0, which is why the innermost curve is a straight segment; SU(3) is a deltoid, SU(4) an astroid. Then the nesting is one line: SU(k) sits in SU(k+1), so pinning one eigenvalue to e^(iφ) gives tr = e^(iφ) + e^(−iφ/k)·(an SU(k) trace) — the smaller region rotated by −φ/k and translated onto the unit circle. Cusp m lands on the larger curve at ψ = (2πm − φ)/k, checked to 9e-15; over the same sweep the worst excursion outside is 2e-15, which is the cusps touching and nothing more. Stacking that map eight times is the picture, and no two of the nine levels turn at the same rate.
// 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);
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 draws four on a real coords frame, and in three of them exactly one member is
the curve with a name: gold is the named one, the three steel-blue siblings are not parabolas,
tractrices or cycloids. That is the point a family makes and a single involute cannot — “the involute
of a semicubic parabola is a parabola” is true for exactly one starting length, the radius of
curvature at the cusp, which here is 1 (checked: |string| = ρ = arc + 1, to 2e-15). The four threads
per panel are collinear, because all four involutes are crossed by the same tangent line of the
spool. Every blue curve cusps ON the spool, because a cusp is where the string length reaches zero.
The string is the RADIUS OF CURVATURE and the spool is the locus of centres of curvature — the
EVOLUTE — so the operations are inverse, checked on all four. Two are exact algebra: the semicubic
parabola IS the evolute of (u, u²/2) to 1e-15, and the cycloid’s involute IS the same cycloid shifted
by (−π, +2r) to 3e-15. The axes let you read it off: the cycloid’s cusp at (0,0), its arch peaking at
2 and its named involute at 4. That panel is Huygens’ clock — 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.
// 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);
cyclogon-hull
cycloid-cyclogon rolled CONVEX polygons, where every vertex takes its turn on the ground. These three do not — a convex quadrilateral, a non-convex one (a dart), and a four-pointed star — and one rule covers all three: A POLYGON ROLLS ON ITS CONVEX HULL. The reflex vertices are passengers; they never touch the line, never take a pivot, and contribute nothing to how far the shape travels. All three hulls have the same 680 px perimeter so all three land together, even though the star’s own boundary is 741 px and the dart’s 712 — a shape’s perimeter has nothing to do with how far it rolls, its hull’s does. The exterior angles that matter are the HULL’s, summing to exactly 360° in all three, which is why one hull perimeter is always one arch: the dart spends that turn in three pivots of ~119°, the star in four of exactly 90° because its hull is a square. The gold point on each row is a hull vertex and touches down every arch; the second point cannot ever reach the line, because a point inside the hull stays inside it — simulated across the whole roll, the dart’s reflex vertex never gets closer than 62.6 px and the star’s inner points than 36.9 px. The faint outline on the lower rows is the hull itself, rolling; the dashed arch is the cycloid a circle of the same rolling perimeter would draw, r = 680/2π. The axes are measured in that radius, and carry only the ticks that mean something — π at the half-arch, a gold 2π where all three marked points land, and 2r on the y-axis exactly where the dashed cycloid peaks and every polygon arch overshoots it.
// 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);
tesseract
The 4-D equivalent of a cube, rotating in four-dimensional space and projected into two for display. Nothing 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 as (axis k, the 8 vertices whose k-th bit is 0) — all 32 distinct, every vertex degree 4 — and every frame is those 4-D points pushed through a 4-D rotation and two perspective divides. The counts come from C(n,k)·2^(n−k): 16 vertices, 32 edges, 24 faces, 8 cells. EVERY EDGE HAS LENGTH 2, always, because a rotation is an isometry and the tesseract is rigid — what changes is the SHADOW, whose projected edge length runs from 0.1507 to 5.4619 across a turn, a factor of 36. Nothing stretches; a shadow is not a length. The motion is a genuine 4-D rotation, not a 3-D one in costume: in four dimensions a rotation has TWO invariant planes, and this turns in x–w at one rate and y–z at twice it. A 3-D rotation always fixes an axis; this fixes only the origin, so no direction is ever standing still. Hue is the fourth coordinate w after rotation, over ±√2 — the apparent inner cube growing into the outer is w passing through the projection. The bottom row is the ladder that makes it legible: a square, a cube’s shadow (a square inside a square), a tesseract’s (a CUBE inside a cube), each one perspective divide further.
// 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);
clifford-torus
A stereographic projection of a Clifford torus performing a simple rotation through the xz plane. 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 — so its Gaussian curvature is identically zero. No closed flat surface can be embedded in R³ at all; it fits in S³ exactly, and the square along the bottom is the real geometry while the shape above is a shadow. THE ROTATION IS SIMPLE: it turns through the xz plane and leaves the orthogonal yw plane fixed POINTWISE (every point moves exactly 0), where a double rotation fixes only the origin. THE PROJECTION IS CONFORMAL, which is why the mesh still meets at right angles — 90° on the torus in S³ and still 90° in R³ after projecting, to 1e-09, at every crossing and every moment; it swells and shrinks wildly and never shears. And EVERY MESH LINE IS AN EXACT CIRCLE, fitted to 2e-14, with radii from 0.414 to 2.414 — the same curve at six times the size depending on where the rotation has put it. The image repeats after HALF a turn, because rotating by π maps the torus to itself by (θ, φ) → (π−θ, π−φ); half a rotation is the whole story. Hue is φ on both, so a line up here is the same line down there.
// 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);
tesseract-sphere
The same hypercube as tesseract.manic, but its edges are SUBDIVIDED and pushed out onto the 3-sphere before anything is projected — the Tesseract article’s “edges projected onto the 3-sphere”, animated. The 16 vertices already lie on S³ exactly; subdividing each edge and normalising every sample is what puts the EDGE on S³ too, and what it lands on is a great circle (each sample coplanar with its endpoints and the origin to 3e-16). Every edge is the SAME arc: adjacent vertices differ in one coordinate so their dot product is exactly 1/2 and the angle exactly 60°, all thirty-two. On the sphere the object is perfectly uniform; the picture is not, and the difference is the projection and nothing else. Stereographic projection sends circles to circles EXCEPT those through the pole, which become lines — every projected edge is planar to 6e-16 and matches a circle or a line to 5e-15, with 120 of 128 circles and 8 lines. The rotation is the third kind: ISOCLINIC, two planes at the SAME rate, which moves EVERY point through exactly the same angle (1e-15) — something no 3-D rotation can do, since there is no axis to be near. It closes after a QUARTER turn, because isoclinic rotation by π/2 maps the vertex set to itself. The pair at the bottom is the comparison: the same 32 edges at the same instant, straight chords on the left and inflated on the right.
// tesseract-sphere — the same hypercube as examples/tesseract.manic, but its edges are
// SUBDIVIDED and pushed out onto the 3-sphere before anything is projected. Every straight
// edge becomes a great-circle arc, and the picture stops being a wireframe of a box and
// becomes a tiling of a sphere.
//
// The construction, in order:
//
// 1. the 16 vertices (±1, ±1, ±1, ±1)/2 already lie on S³ — exactly, ‖v‖ − 1 = 0
// 2. every edge is subdivided, and each sample is normalised onto S³
// 3. the whole thing turns by an ISOCLINIC rotation
// 4. stereographic projection from the pole (0, 0, 0, 1) brings it to R³
// 5. an ordinary view projects R³ to the page
//
// Step 2 is the whole difference. A chord of a sphere is not on the sphere; subdividing it
// and normalising each piece is what puts the edge ON S³, and what it lands on is a great
// circle — checked, every sample is coplanar with its two endpoints and the origin (Gram
// determinant 3e-16) and has ‖p‖ = 1 to 1e-16. A plane through the origin cuts S³ in a great
// circle, so each edge is an arc of one.
//
// AND EVERY EDGE IS THE SAME ARC. Two adjacent vertices differ in one coordinate, so their
// dot product is exactly 1/2 and the angle between them is exactly 60°. All thirty-two of
// them. On the sphere this object is completely uniform; the picture is not, and that
// difference is the projection and nothing else.
//
// What the arcs become in R³ is the classical fact about stereographic projection: circles
// go to circles, EXCEPT those through the pole, which go to straight lines. Fitted to their
// own images, every projected edge is planar to 6e-16 and matches a circle or a line to
// 5e-15 — over four rotation angles, 120 of 128 came out circles and 8 came out lines, and
// the 8 are exactly the arcs whose great circle passes through the pole. Circle radii run
// from 1.00 to 3.38, so identical arcs are drawn at three times each other's size.
//
// THE ROTATION IS ISOCLINIC, the third kind. A SIMPLE rotation turns in one plane and fixes
// the other pointwise (examples/clifford-torus.manic). A DOUBLE rotation turns in two planes
// at different rates and fixes only the origin (examples/tesseract.manic). An ISOCLINIC
// rotation turns in two planes at the SAME rate, and it has a property neither of the others
// has and no rotation in three dimensions can have:
//
// EVERY POINT MOVES THROUGH EXACTLY THE SAME ANGLE.
//
// Checked to 1e-15, at every point and every angle. In three dimensions a point near the
// axis barely moves and a point far from it moves most; here nothing is near an axis,
// because there is no axis.
//
// It closes after a QUARTER turn: an isoclinic rotation by π/2 sends (x₁,x₂,x₃,x₄) to
// (−x₂, x₁, −x₄, x₃), which maps the vertex set to itself. So π/2 is the whole story.
//
// The pair at the bottom is the comparison: the same 32 edges at the same instant, drawn as
// straight chords on the left and inflated onto the sphere on the right. Only step 2 differs.
//
// Hue is the fourth coordinate after the rotation — the one the projection consumes.
//
// manic examples/tesseract-sphere.manic
title("A hypercube subdivided onto the 3-sphere");
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 hypercube, subdivided onto the 3-sphere", (540, 116), 27);
// ---- the hero: 32 edges, each an arc of a great circle -----------------------------
cloud(arcs, 7040, #ffffff, 1.0) {
let per = 220;
let ed = (i - mod(i, per))/per; // which of the 32 edges
let v = mod(i, per)/219;
let kk = (ed - mod(ed, 8))/8; // the axis it runs along
let jj = mod(ed, 8);
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);
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;
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 nn = sqrt(c0*c0 + c1*c1 + c2*c2 + c3*c3); // SUBDIVIDE, then inflate onto S^3
let d0 = c0/nn;
let d1 = c1/nn;
let d2 = c2/nn;
let d3 = c3/nn;
let al = 1.5707963*min(t/12.0, 1); // ISOCLINIC: both planes at the SAME rate
let e0 = d0*cos(al) - d1*sin(al);
let e1 = d0*sin(al) + d1*cos(al);
let e2 = d2*cos(al) - d3*sin(al);
let e3 = d2*sin(al) + d3*cos(al);
let sp = 1/(1 - e3); // stereographic, from the pole (0,0,0,1)
let ax = e0*sp;
let ay = e1*sp;
let az = e2*sp;
let bx = ax*cos(0.62) - az*sin(0.62);
let bz = ax*sin(0.62) + az*cos(0.62);
let ey = ay*cos(0.38) - bz*sin(0.38);
let ez = ay*sin(0.38) + bz*cos(0.38);
let m3 = 8/(8 - ez);
let x = 540 + 129*bx*m3;
let y = 700 - 129*ey*m3;
let hue = mod(188 + e3*120, 360);
let sat = 0.78;
let r = 2.0;
let alpha = 0.32 + 0.6*min(max((ez + 3)/6, 0), 1);
}
// ---- the same edges, straight and inflated, side by side ---------------------------
cloud(chords, 2240, #ffffff, 1.0) {
let per = 70;
let ed = (i - mod(i, per))/per; // which of the 32 edges
let v = mod(i, per)/69;
let kk = (ed - mod(ed, 8))/8; // the axis it runs along
let jj = mod(ed, 8);
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);
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;
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 d0 = c0*0.5; // left as straight chords, not arcs
let d1 = c1*0.5;
let d2 = c2*0.5;
let d3 = c3*0.5;
let al = 1.5707963*min(t/12.0, 1); // ISOCLINIC: both planes at the SAME rate
let e0 = d0*cos(al) - d1*sin(al);
let e1 = d0*sin(al) + d1*cos(al);
let e2 = d2*cos(al) - d3*sin(al);
let e3 = d2*sin(al) + d3*cos(al);
let sp = 1/(1 - e3); // stereographic, from the pole (0,0,0,1)
let ax = e0*sp;
let ay = e1*sp;
let az = e2*sp;
let bx = ax*cos(0.62) - az*sin(0.62);
let bz = ax*sin(0.62) + az*cos(0.62);
let ey = ay*cos(0.38) - bz*sin(0.38);
let ez = ay*sin(0.38) + bz*cos(0.38);
let m3 = 8/(8 - ez);
let x = 250 + 58*bx*m3;
let y = 1470 - 58*ey*m3;
let hue = mod(188 + e3*120, 360);
let sat = 0.78;
let r = 1.5;
let alpha = 0.32 + 0.6*min(max((ez + 3)/6, 0), 1);
}
cloud(onsph, 2240, #ffffff, 1.0) {
let per = 70;
let ed = (i - mod(i, per))/per; // which of the 32 edges
let v = mod(i, per)/69;
let kk = (ed - mod(ed, 8))/8; // the axis it runs along
let jj = mod(ed, 8);
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);
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;
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 nn = sqrt(c0*c0 + c1*c1 + c2*c2 + c3*c3); // SUBDIVIDE, then inflate onto S^3
let d0 = c0/nn;
let d1 = c1/nn;
let d2 = c2/nn;
let d3 = c3/nn;
let al = 1.5707963*min(t/12.0, 1); // ISOCLINIC: both planes at the SAME rate
let e0 = d0*cos(al) - d1*sin(al);
let e1 = d0*sin(al) + d1*cos(al);
let e2 = d2*cos(al) - d3*sin(al);
let e3 = d2*sin(al) + d3*cos(al);
let sp = 1/(1 - e3); // stereographic, from the pole (0,0,0,1)
let ax = e0*sp;
let ay = e1*sp;
let az = e2*sp;
let bx = ax*cos(0.62) - az*sin(0.62);
let bz = ax*sin(0.62) + az*cos(0.62);
let ey = ay*cos(0.38) - bz*sin(0.38);
let ez = ay*sin(0.38) + bz*cos(0.38);
let m3 = 8/(8 - ez);
let x = 830 + 58*bx*m3;
let y = 1470 - 58*ey*m3;
let hue = mod(188 + e3*120, 360);
let sat = 0.78;
let r = 1.5;
let alpha = 0.32 + 0.6*min(max((ez + 3)/6, 0), 1);
}
equation(eq, (540, 1760),
`\cos\angle(v_i,v_j)=\tfrac{1}{2}\;\Longrightarrow\;\text{every edge is a }60^\circ\text{ arc}`, 24);
wait(15.0);
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 with the original’s labels — and the thing a static picture cannot show, which way each one is going. From x = sin 2πt, y = sin(2πt − δ), eliminating t leaves a conic for every δ (x² − 2xy·cos δ + y² = sin²δ, residual 1e-15) whose axes are the ±45° diagonals with semi-axes A = √(1+cos δ) and B = √(1−cos δ). Everything in the original comes out of that as TWO SIGN BITS. A² − B² = 2·cos δ exactly, so cos δ decides which diagonal the major axis lies on — the SLOPE. And x·ẏ − y·ẋ = 2π·sin δ, with no t in it (8e-10), so sin δ decides the DIRECTION, which therefore can never reverse partway round — and the curve is swept at constant areal rate, equal areas in equal times, for every δ. Two signs, four quadrants, exactly the braces in the source: positive slope (I & IV), negative slope (II & III), clockwise (I & II), counter clockwise (III & IV). Gold is clockwise, cyan counter clockwise, white the two where sin δ = 0 and the oval has collapsed to a line. Reading a phase off the SHAPE alone is ambiguous — −45° and −135° share an eccentricity of 0.9102, tilted opposite ways — so the moving dot is the only thing that separates them, which is why animating this figure is not decoration. At the centre δ sweeps continuously, so the ring is eight samples of the thing turning in the middle.
// 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);
lissajous-grid
v2 of lissajous-phase, which took one ratio (1:1) and swept the phase. This takes the other axis of the same figure: EIGHT frequency ratios against FIVE phases, reproducing the table from Tyndall’s “Sound” (1867) that the article carries as Lissajous_relaciones.png — x = cos(nt + δ), y = cos(mt) for the row m:n. The first and last columns are open arcs and everything between is a closed loop, in every row, and that is not luck: the curve retraces itself exactly when δ = jπ − knπ/m, and j = k = 0 gives δ = 0 while j = 1, k = 0 gives δ = π — available for every m and n. Those columns are drawn paler to say so. At 1:1 they are straight lines, at 1:2 parabolas, at 1:3 cubics: the Chebyshev curves x = T(y), which is what a Lissajous becomes when the phase lets it collapse. The bottom row is the article’s other sentence — “a finite sum of the first 100, 1000 and 5000 prime number frequencies”. Neither the article nor the file page gives a formula, so the scene states its own: the path of the partial sums of Σ exp(2πi·pₖ·t) at t = 1/6 + 1/4000. Near 1/6 is the whole point — every prime past 3 is 6k ± 1, so 4998 of the first 5000 sit in two residue classes and the walk has only two step directions, which the offset makes creep round into spirals. A random walk of N unit steps ends about √N from home; these reach 4.6, 3.0 and 1.6 times that, and all three are drawn at the same scale so the growth on screen is the growth in the sum.
// lissajous-grid — v2 of examples/lissajous-phase.manic, which took one frequency ratio
// (1:1) and swept the phase. This takes the other axis of the same figure: EIGHT ratios
// against FIVE phases, reproducing the table from Tyndall's "Sound" (1867) that the
// Wikipedia article carries as Lissajous_relaciones.png.
//
// x(t) = cos(n·t + δ) y(t) = cos(m·t) for the row labelled m:n
//
// The row label puts m on y and n on x, so "1:2" means x runs twice for every y — which is
// why that row shows a curve two lobes wide, not two tall.
//
// The first and last columns are open arcs and every column between them is a closed loop,
// in every row. That is not a coincidence of the eight ratios chosen; it follows. The curve
// retraces itself exactly when cos(m·s) = cos(m·t) and cos(n·s + δ) = cos(n·t + δ) have a
// common solution s ≠ t, which works out to
//
// δ = jπ − k·nπ/m for integers j, k
//
// and j = k = 0 gives δ = 0 while j = 1, k = 0 gives δ = π — available for EVERY m and n.
// So the two ends of the phase row are degenerate whatever the ratio is, and the figure's
// left and right columns are open for that reason. They are drawn paler here to say so.
//
// At 1:1 the open cases are the two straight lines of examples/lissajous-phase.manic. At 1:2
// they are parabolas, at 1:3 cubics — these are the Chebyshev curves x = T_{n/m}(y), which
// is what a Lissajous figure becomes when the phase lets it collapse.
//
// ---------------------------------------------------------------------------------------
//
// The row along the bottom is the article's other sentence: "aesthetically interesting
// Lissajous curves with a finite sum of the first 100, 1000 and 5000 prime number
// frequencies". Neither the article nor the file page gives a formula, so this is one
// choice, stated plainly:
//
// S_N = Σ (k = 1 … N) exp(2πi·p_k·t), drawn as the path of its partial sums
//
// with t = 1/6 + 1/4000 and N = 100, 1000, 5000. Summing prime frequencies is exactly what
// it does, and the path has N steps, so the three numbers in the caption are the three
// pictures.
//
// Choosing t near 1/6 is the whole reason it is worth looking at. EVERY PRIME PAST 3 IS
// 6k ± 1 — 6k, 6k+2, 6k+3 and 6k+4 are all composite — so among the first 5000 primes,
// 4998 of them are ≡ ±1 (mod 6) and exactly two are not, namely 2 and 3. At t = 1/6 the walk
// therefore has only TWO step directions. Offset t slightly and each direction creeps round,
// and the two directions wind into two interleaved spirals.
//
// It shows in the numbers. A random walk of N unit steps ends about √N from home; these
// reach 4.6, 3.0 and 1.6 times that at N = 100, 1000 and 5000, and all three are drawn at Away from a rational the
// the SAME scale, so the growth on screen is the growth in the sum. Away from a rational the
// same walk has no structure at all — what is special here is not the primes, it is 6 being
// special to the primes.
//
// manic examples/lissajous-grid.manic
title("Lissajous: eight frequency ratios against five phases");
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);
caption(head, "Eight frequency ratios, five phases", (540, 116), 26);
text(ch0, (300, 208), "0"); size(ch0, 25); color(ch0, fg);
text(ch1, (450, 208), "π/4"); size(ch1, 25); color(ch1, fg);
text(ch2, (600, 208), "π/2"); size(ch2, 25); color(ch2, fg);
text(ch3, (750, 208), "3π/4"); size(ch3, 25); color(ch3, fg);
text(ch4, (900, 208), "π"); size(ch4, 25); color(ch4, fg);
text(rh0, (168, 285), "1:1"); size(rh0, 24); color(rh0, fg);
text(rh1, (168, 411), "1:2"); size(rh1, 24); color(rh1, fg);
text(rh2, (168, 537), "1:3"); size(rh2, 24); color(rh2, fg);
text(rh3, (168, 663), "2:3"); size(rh3, 24); color(rh3, fg);
text(rh4, (168, 789), "3:4"); size(rh4, 24); color(rh4, fg);
text(rh5, (168, 915), "3:5"); size(rh5, 24); color(rh5, fg);
text(rh6, (168, 1041), "4:5"); size(rh6, 24); color(rh6, fg);
text(rh7, (168, 1167), "5:6"); size(rh7, 24); color(rh7, fg);
// ---- the table: 40 cells, drawn on together ----------------------------------------
cloud(grid, 16800, #ffffff, 1.0) {
let per = 420;
let ci = (i - mod(i, per))/per; // which cell, 0..39
let rw = (ci - mod(ci, 5))/5; // its row …
let cl = mod(ci, 5); // … and column
let mm = (1 - min(abs(rw - 0), 1))*1 + (1 - min(abs(rw - 1), 1))*1 + (1 - min(abs(rw - 2), 1))*1 + (1 - min(abs(rw - 3), 1))*2 + (1 - min(abs(rw - 4), 1))*3 + (1 - min(abs(rw - 5), 1))*3 + (1 - min(abs(rw - 6), 1))*4 + (1 - min(abs(rw - 7), 1))*5;
let nn = (1 - min(abs(rw - 0), 1))*1 + (1 - min(abs(rw - 1), 1))*2 + (1 - min(abs(rw - 2), 1))*3 + (1 - min(abs(rw - 3), 1))*3 + (1 - min(abs(rw - 4), 1))*4 + (1 - min(abs(rw - 5), 1))*5 + (1 - min(abs(rw - 6), 1))*5 + (1 - min(abs(rw - 7), 1))*6;
let dl = cl*0.7853982;
let go = min(t/14.0, 1);
let th = 6.283185*go*(mod(i, per)/419);
let dg = (1 - min(abs(cl - 0), 1)) + (1 - min(abs(cl - 4), 1));
let x = 300 + 150*cl + 56*cos(nn*th + dl);
let y = 285 + 126*rw - 56*cos(mm*th);
let hue = 20 + rw*38;
let sat = 0.75 - 0.52*dg; // the two degenerate columns, paler
let r = 1.7;
}
cloud(nibs, 1600, #ffffff, 1.0) {
let per = 40;
let ci = (i - mod(i, per))/per;
let rw = (ci - mod(ci, 5))/5;
let cl = mod(ci, 5);
let mm = (1 - min(abs(rw - 0), 1))*1 + (1 - min(abs(rw - 1), 1))*1 + (1 - min(abs(rw - 2), 1))*1 + (1 - min(abs(rw - 3), 1))*2 + (1 - min(abs(rw - 4), 1))*3 + (1 - min(abs(rw - 5), 1))*3 + (1 - min(abs(rw - 6), 1))*4 + (1 - min(abs(rw - 7), 1))*5;
let nn = (1 - min(abs(rw - 0), 1))*1 + (1 - min(abs(rw - 1), 1))*2 + (1 - min(abs(rw - 2), 1))*3 + (1 - min(abs(rw - 3), 1))*3 + (1 - min(abs(rw - 4), 1))*4 + (1 - min(abs(rw - 5), 1))*5 + (1 - min(abs(rw - 6), 1))*5 + (1 - min(abs(rw - 7), 1))*6;
let dl = cl*0.7853982;
let th = 6.283185*min(t/14.0, 1);
let a = mod(i, per)/40*6.283185;
let rr = 5*(mod(i, per)/40);
let x = 300 + 150*cl + 56*cos(nn*th + dl) + rr*cos(a*9);
let y = 285 + 126*rw - 56*cos(mm*th) + rr*sin(a*9);
let hue = 48;
let sat = 0.5;
let r = 1.9;
}
// ---- the prime-frequency walks -----------------------------------------------------
polygon(w100, (216.1, 1441.3), (215.3, 1439.9), (213.7, 1439.9), (214.6, 1441.3), (215.3, 1439.9), (216.1, 1441.3), (216.9, 1439.9), (217.7, 1441.2), (218.5, 1439.8), (219.3, 1441.2), (220.2, 1442.5), (220.9, 1441.1), (221.6, 1439.7), (222.5, 1441.0), (223.2, 1439.6), (224.1, 1440.9), (225.0, 1442.2), (225.9, 1443.5), (226.6, 1442.0), (227.2, 1440.6), (228.1, 1441.9), (228.8, 1440.4), (229.4, 1439.0), (230.4, 1440.2), (231.3, 1441.5), (231.9, 1440.0), (232.9, 1441.2), (233.5, 1439.7), (234.5, 1441.0), (235.0, 1439.5), (236.0, 1440.7), (236.5, 1439.2), (237.6, 1440.4), (238.7, 1441.5), (239.1, 1440.0), (240.2, 1441.2), (240.7, 1439.7), (241.1, 1438.1), (241.5, 1436.6), (242.6, 1437.7), (243.8, 1438.8), (244.9, 1439.9), (245.3, 1438.4), (246.5, 1439.5), (246.8, 1437.9), (248.0, 1439.0), (248.3, 1437.4), (248.6, 1435.9), (248.9, 1434.3), (250.1, 1435.3), (250.4, 1433.8), (251.6, 1434.8), (252.8, 1435.8), (253.1, 1434.2), (254.3, 1435.1), (255.6, 1436.1), (256.9, 1437.0), (258.1, 1438.0), (258.3, 1436.4), (258.4, 1434.8), (259.7, 1435.7), (259.9, 1434.1), (261.2, 1435.0), (261.3, 1433.4), (262.6, 1434.3), (262.6, 1432.7), (264.0, 1433.5), (264.0, 1431.9), (264.0, 1430.3), (265.4, 1431.1), (265.3, 1429.5), (266.7, 1430.3), (268.1, 1431.0), (268.1, 1429.4), (268.0, 1427.8), (267.8, 1426.3), (269.3, 1426.9), (270.7, 1427.6), (270.6, 1426.0), (272.0, 1426.7), (271.8, 1425.1), (273.3, 1425.7), (273.1, 1424.1), (274.5, 1424.7), (274.3, 1423.2), (274.0, 1421.6), (275.5, 1422.1), (277.0, 1422.7), (276.7, 1421.1), (278.2, 1421.6), (277.9, 1420.1), (279.4, 1420.6), (280.9, 1421.0), (280.5, 1419.5), (282.0, 1419.9), (281.6, 1418.4), (283.2, 1418.8), (284.7, 1419.2), (286.3, 1419.5), (285.8, 1418.0), (285.3, 1416.5), (285.8, 1418.0), (286.3, 1419.5), (284.7, 1419.2), (283.2, 1418.8), (281.6, 1418.4), (282.0, 1419.9), (280.5, 1419.5), (280.9, 1421.0), (279.4, 1420.6), (277.9, 1420.1), (278.2, 1421.6), (276.7, 1421.1), (277.0, 1422.7), (275.5, 1422.1), (274.0, 1421.6), (274.3, 1423.2), (274.5, 1424.7), (273.1, 1424.1), (273.3, 1425.7), (271.8, 1425.1), (272.0, 1426.7), (270.6, 1426.0), (270.7, 1427.6), (269.3, 1426.9), (267.8, 1426.3), (268.0, 1427.8), (268.1, 1429.4), (268.1, 1431.0), (266.7, 1430.3), (265.3, 1429.5), (265.4, 1431.1), (264.0, 1430.3), (264.0, 1431.9), (264.0, 1433.5), (262.6, 1432.7), (262.6, 1434.3), (261.3, 1433.4), (261.2, 1435.0), (259.9, 1434.1), (259.7, 1435.7), (258.4, 1434.8), (258.3, 1436.4), (258.1, 1438.0), (256.9, 1437.0), (255.6, 1436.1), (254.3, 1435.1), (253.1, 1434.2), (252.8, 1435.8), (251.6, 1434.8), (250.4, 1433.8), (250.1, 1435.3), (248.9, 1434.3), (248.6, 1435.9), (248.3, 1437.4), (248.0, 1439.0), (246.8, 1437.9), (246.5, 1439.5), (245.3, 1438.4), (244.9, 1439.9), (243.8, 1438.8), (242.6, 1437.7), (241.5, 1436.6), (241.1, 1438.1), (240.7, 1439.7), (240.2, 1441.2), (239.1, 1440.0), (238.7, 1441.5), (237.6, 1440.4), (236.5, 1439.2), (236.0, 1440.7), (235.0, 1439.5), (234.5, 1441.0), (233.5, 1439.7), (232.9, 1441.2), (231.9, 1440.0), (231.3, 1441.5), (230.4, 1440.2), (229.4, 1439.0), (228.8, 1440.4), (228.1, 1441.9), (227.2, 1440.6), (226.6, 1442.0), (225.9, 1443.5), (225.0, 1442.2), (224.1, 1440.9), (223.2, 1439.6), (222.5, 1441.0), (221.6, 1439.7), (220.9, 1441.1), (220.2, 1442.5), (219.3, 1441.2), (218.5, 1439.8), (217.7, 1441.2), (216.9, 1439.9), (216.1, 1441.3), (215.3, 1439.9), (214.6, 1441.3), (213.7, 1439.9), (215.3, 1439.9), (216.1, 1441.3));
outlined(w100); outline(w100, mint); stroke(w100, 2);
text(wl100, (250, 1572), "first 100 primes"); size(wl100, 20); color(wl100, dim);
polygon(w1000, (519.0, 1501.4), (516.6, 1500.1), (518.2, 1500.1), (519.8, 1500.0), (521.4, 1500.0), (523.1, 1502.7), (524.5, 1499.8), (526.1, 1499.7), (527.9, 1502.4), (529.4, 1502.2), (531.0, 1502.0), (532.3, 1499.1), (534.2, 1501.6), (535.8, 1501.4), (537.3, 1501.1), (538.9, 1500.8), (540.5, 1500.5), (542.0, 1500.2), (543.6, 1499.8), (544.4, 1496.8), (546.7, 1499.0), (548.2, 1498.5), (549.7, 1498.1), (551.2, 1497.6), (551.8, 1494.5), (553.2, 1493.9), (555.7, 1495.9), (557.2, 1495.3), (559.7, 1497.2), (561.2, 1496.5), (562.6, 1495.9), (564.1, 1495.2), (565.5, 1494.4), (566.9, 1493.7), (566.9, 1490.5), (568.2, 1489.7), (571.0, 1491.2), (570.8, 1488.0), (572.2, 1487.1), (573.4, 1486.2), (574.7, 1485.3), (575.9, 1484.3), (577.2, 1483.3), (578.4, 1482.3), (579.6, 1481.3), (580.8, 1480.2), (583.8, 1481.2), (584.9, 1480.1), (586.0, 1478.9), (589.1, 1479.7), (588.2, 1476.7), (589.2, 1475.4), (592.3, 1475.9), (591.2, 1473.0), (594.3, 1473.4), (595.2, 1472.1), (593.9, 1469.2), (594.8, 1467.9), (595.7, 1466.6), (596.5, 1465.2), (599.7, 1465.3), (598.1, 1462.5), (601.2, 1462.5), (602.0, 1461.0), (602.7, 1459.6), (600.8, 1457.0), (601.5, 1455.6), (599.5, 1453.1), (600.1, 1451.6), (600.6, 1450.1), (603.7, 1449.5), (604.2, 1447.9), (604.6, 1446.4), (605.0, 1444.8), (605.4, 1443.3), (605.7, 1441.7), (606.0, 1440.2), (606.3, 1438.6), (606.5, 1437.0), (606.7, 1435.4), (606.9, 1433.9), (609.7, 1432.5), (609.8, 1430.9), (612.6, 1429.4), (609.9, 1427.8), (609.8, 1426.2), (609.8, 1424.6), (609.7, 1423.0), (609.6, 1421.4), (609.5, 1419.8), (606.6, 1418.6), (606.3, 1417.0), (606.1, 1415.4), (608.6, 1413.4), (605.6, 1412.3), (605.2, 1410.8), (604.8, 1409.2), (604.4, 1407.7), (606.5, 1405.3), (603.4, 1404.7), (605.5, 1402.3), (604.9, 1400.8), (601.8, 1400.4), (603.6, 1397.8), (602.9, 1396.4), (602.3, 1394.9), (601.5, 1393.5), (600.8, 1392.1), (600.0, 1390.7), (599.2, 1389.3), (600.6, 1386.5), (597.5, 1386.8), (596.5, 1385.5), (595.5, 1384.3), (596.7, 1381.3), (595.6, 1380.1), (592.5, 1380.7), (591.4, 1379.5), (590.3, 1378.4), (589.2, 1377.2), (590.0, 1374.2), (588.9, 1373.1), (585.9, 1374.2), (586.4, 1371.0), (585.2, 1370.0), (583.9, 1369.1), (582.6, 1368.1), (581.3, 1367.2), (581.6, 1364.1), (578.7, 1365.5), (577.4, 1364.6), (576.1, 1363.8), (573.3, 1365.5), (571.9, 1364.8), (571.7, 1361.6), (570.2, 1360.9), (567.7, 1362.9), (565.2, 1364.8), (562.8, 1366.8), (561.3, 1366.3), (559.8, 1365.9), (558.3, 1365.4), (556.7, 1365.1), (555.2, 1364.8), (553.6, 1364.4), (552.1, 1364.1), (550.9, 1361.2), (549.7, 1358.3), (548.0, 1358.1), (546.4, 1358.0), (544.9, 1358.0), (543.3, 1358.0), (541.7, 1358.0), (540.1, 1360.8), (538.6, 1360.8), (537.0, 1360.9), (535.2, 1358.4), (533.6, 1358.6), (532.0, 1358.8), (530.0, 1356.4), (528.4, 1356.7), (527.5, 1359.7), (525.9, 1360.0), (524.4, 1360.4), (523.7, 1363.5), (521.3, 1361.4), (519.8, 1362.0), (518.3, 1362.6), (516.8, 1363.2), (515.4, 1363.9), (515.1, 1367.0), (513.7, 1367.7), (512.3, 1368.5), (509.6, 1366.9), (509.6, 1370.1), (506.8, 1368.6), (507.0, 1371.7), (505.7, 1372.6), (504.4, 1373.5), (501.5, 1372.3), (498.6, 1371.1), (497.4, 1372.1), (494.4, 1371.1), (495.0, 1374.1), (493.9, 1375.2), (492.9, 1376.4), (491.8, 1377.6), (490.8, 1378.8), (489.8, 1380.1), (488.8, 1381.3), (487.9, 1382.6), (484.8, 1382.4), (483.9, 1383.8), (483.1, 1385.2), (484.7, 1387.9), (484.0, 1389.3), (483.2, 1390.7), (482.5, 1392.1), (481.8, 1393.5), (481.2, 1395.0), (480.5, 1396.4), (479.9, 1397.9), (479.3, 1399.4), (478.8, 1400.9), (478.3, 1402.4), (477.9, 1404.0), (477.5, 1405.5), (477.1, 1407.0), (476.7, 1408.6), (476.4, 1410.1), (473.5, 1411.3), (473.3, 1412.8), (470.4, 1414.1), (470.3, 1415.7), (467.4, 1417.1), (467.4, 1418.8), (467.4, 1420.4), (467.5, 1422.0), (467.6, 1423.5), (470.4, 1424.9), (473.3, 1426.1), (470.8, 1428.1), (471.1, 1429.7), (471.5, 1431.3), (471.9, 1432.8), (474.9, 1433.6), (475.4, 1435.1), (473.3, 1437.5), (473.8, 1439.0), (474.4, 1440.5), (475.1, 1441.9), (478.2, 1442.3), (478.9, 1443.7), (482.1, 1443.8), (482.9, 1445.2), (483.6, 1446.6), (484.5, 1447.9), (485.3, 1449.3), (486.2, 1450.6), (487.1, 1451.9), (485.8, 1454.8), (486.9, 1456.0), (487.9, 1457.2), (489.0, 1458.4), (490.1, 1459.6), (493.2, 1458.8), (494.3, 1459.9), (495.5, 1460.9), (496.7, 1462.0), (497.9, 1463.0), (500.9, 1461.9), (502.1, 1462.9), (503.4, 1463.8), (504.7, 1464.7), (506.0, 1465.5), (508.8, 1464.1), (508.8, 1467.2), (510.2, 1468.0), (512.9, 1466.3), (514.3, 1467.0), (515.7, 1467.7), (518.2, 1465.8), (518.7, 1468.9), (520.2, 1469.5), (521.7, 1470.0), (522.5, 1473.1), (524.0, 1473.5), (525.6, 1473.9), (527.1, 1474.2), (528.7, 1474.5), (530.7, 1472.1), (531.9, 1475.0), (533.5, 1475.2), (535.0, 1475.4), (536.6, 1475.5), (538.2, 1475.6), (539.8, 1475.6), (541.5, 1478.3), (543.0, 1478.2), (544.7, 1478.1), (546.0, 1475.3), (547.9, 1477.8), (549.5, 1477.6), (550.6, 1474.6), (552.1, 1474.3), (554.3, 1476.7), (555.8, 1476.3), (556.6, 1473.2), (558.1, 1472.7), (559.6, 1472.2), (561.1, 1471.6), (562.6, 1471.0), (565.1, 1472.9), (566.6, 1472.3), (568.0, 1471.6), (569.4, 1470.9), (570.8, 1470.1), (572.2, 1469.3), (572.1, 1466.1), (574.9, 1467.5), (577.8, 1468.8), (579.1, 1467.8), (578.5, 1464.7), (581.5, 1465.7), (584.5, 1466.7), (585.7, 1465.5), (584.8, 1462.5), (585.9, 1461.3), (589.0, 1462.0), (587.9, 1459.0), (588.9, 1457.7), (587.7, 1454.8), (586.3, 1452.0), (587.2, 1450.6), (590.3, 1450.7), (588.8, 1448.0), (589.6, 1446.6), (592.8, 1446.5), (595.9, 1446.3), (594.1, 1443.7), (597.2, 1443.3), (595.2, 1440.9), (595.7, 1439.4), (598.8, 1438.7), (596.7, 1436.4), (597.1, 1434.9), (597.5, 1433.3), (600.4, 1432.3), (598.0, 1430.3), (600.9, 1429.1), (601.0, 1427.5), (601.2, 1425.9), (601.3, 1424.3), (601.3, 1422.7), (598.6, 1421.1), (598.6, 1419.5), (598.6, 1417.9), (598.5, 1416.3), (601.2, 1414.6), (598.3, 1413.3), (600.8, 1411.4), (600.5, 1409.8), (597.6, 1408.7), (597.2, 1407.1), (596.8, 1405.6), (596.4, 1404.1), (595.9, 1402.6), (592.8, 1402.0), (592.3, 1400.5), (594.2, 1398.0), (596.0, 1395.4), (595.3, 1394.0), (597.0, 1391.3), (596.2, 1389.9), (595.4, 1388.6), (596.7, 1385.7), (598.1, 1382.8), (594.9, 1383.2), (593.9, 1381.9), (590.8, 1382.4), (589.8, 1381.2), (588.7, 1380.0), (587.6, 1378.9), (588.5, 1375.8), (587.4, 1374.7), (586.2, 1373.6), (585.0, 1372.5), (582.1, 1373.6), (580.8, 1372.6), (579.6, 1371.7), (578.2, 1370.8), (575.4, 1372.3), (574.1, 1371.5), (572.7, 1370.6), (571.3, 1369.9), (569.9, 1369.1), (568.5, 1368.4), (567.1, 1367.8), (565.6, 1367.2), (563.2, 1369.1), (561.7, 1368.6), (561.0, 1365.5), (559.5, 1365.1), (557.3, 1367.3), (556.4, 1364.3), (554.2, 1366.6), (553.3, 1363.6), (551.7, 1363.3), (550.1, 1363.0), (548.6, 1362.8), (547.3, 1359.9), (545.6, 1359.8), (544.1, 1359.8), (542.4, 1357.1), (541.0, 1359.9), (539.2, 1357.3), (538.0, 1360.2), (536.4, 1360.4), (534.8, 1360.7), (533.2, 1361.0), (531.7, 1361.3), (530.2, 1361.7), (528.6, 1362.1), (527.1, 1362.6), (525.6, 1363.1), (524.1, 1363.6), (523.7, 1366.7), (521.1, 1364.8), (519.7, 1365.5), (518.2, 1366.2), (516.8, 1366.9), (514.1, 1365.2), (511.4, 1363.6), (511.4, 1366.8), (508.6, 1365.3), (508.8, 1368.5), (509.0, 1371.6), (507.7, 1372.5), (508.2, 1375.7), (507.0, 1376.7), (505.8, 1377.8), (504.6, 1378.9), (503.6, 1380.0), (502.5, 1381.2), (501.5, 1382.5), (500.5, 1383.7), (499.5, 1384.9), (498.6, 1386.2), (500.0, 1389.1), (496.9, 1389.0), (498.5, 1391.7), (497.7, 1393.2), (497.0, 1394.6), (498.7, 1397.2), (498.1, 1398.6), (494.9, 1399.0), (494.3, 1400.6), (493.8, 1402.1), (493.3, 1403.6), (492.9, 1405.1), (492.5, 1406.7), (492.1, 1408.2), (489.1, 1409.2), (491.5, 1411.3), (491.2, 1412.8), (488.3, 1414.0), (488.2, 1415.6), (488.1, 1417.2), (488.0, 1418.8), (485.2, 1420.3), (487.9, 1421.9), (485.2, 1423.5), (485.3, 1425.1), (485.4, 1426.7), (488.2, 1428.0), (485.8, 1429.9), (483.3, 1432.0), (483.7, 1433.5), (484.0, 1435.0), (484.5, 1436.6), (485.0, 1438.1), (485.5, 1439.5), (486.0, 1441.1), (489.2, 1441.5), (487.2, 1444.0), (487.9, 1445.4), (488.7, 1446.8), (489.4, 1448.2), (490.3, 1449.6), (493.4, 1449.5), (496.6, 1449.1), (497.6, 1450.4), (496.5, 1453.4), (499.6, 1452.7), (498.7, 1455.8), (499.9, 1456.9), (501.0, 1458.0), (500.3, 1461.1), (501.5, 1462.2), (502.7, 1463.2), (503.9, 1464.2), (505.2, 1465.2), (504.8, 1468.3), (507.7, 1467.1), (509.0, 1468.0), (510.3, 1468.8), (511.6, 1469.6), (513.0, 1470.4), (514.4, 1471.1), (515.8, 1471.8), (518.4, 1470.0), (519.9, 1470.6), (521.3, 1471.2), (523.8, 1469.2), (524.5, 1472.3), (525.2, 1475.4), (526.1, 1478.4), (528.2, 1476.1), (529.8, 1476.4), (530.9, 1479.3), (532.1, 1482.3), (530.9, 1479.3), (529.8, 1476.4), (528.2, 1476.1), (526.1, 1478.4), (525.2, 1475.4), (524.5, 1472.3), (523.8, 1469.2), (521.3, 1471.2), (519.9, 1470.6), (518.4, 1470.0), (515.8, 1471.8), (514.4, 1471.1), (513.0, 1470.4), (511.6, 1469.6), (510.3, 1468.8), (509.0, 1468.0), (507.7, 1467.1), (504.8, 1468.3), (505.2, 1465.2), (503.9, 1464.2), (502.7, 1463.2), (501.5, 1462.2), (500.3, 1461.1), (501.0, 1458.0), (499.9, 1456.9), (498.7, 1455.8), (499.6, 1452.7), (496.5, 1453.4), (497.6, 1450.4), (496.6, 1449.1), (493.4, 1449.5), (490.3, 1449.6), (489.4, 1448.2), (488.7, 1446.8), (487.9, 1445.4), (487.2, 1444.0), (489.2, 1441.5), (486.0, 1441.1), (485.5, 1439.5), (485.0, 1438.1), (484.5, 1436.6), (484.0, 1435.0), (483.7, 1433.5), (483.3, 1432.0), (485.8, 1429.9), (488.2, 1428.0), (485.4, 1426.7), (485.3, 1425.1), (485.2, 1423.5), (487.9, 1421.9), (485.2, 1420.3), (488.0, 1418.8), (488.1, 1417.2), (488.2, 1415.6), (488.3, 1414.0), (491.2, 1412.8), (491.5, 1411.3), (489.1, 1409.2), (492.1, 1408.2), (492.5, 1406.7), (492.9, 1405.1), (493.3, 1403.6), (493.8, 1402.1), (494.3, 1400.6), (494.9, 1399.0), (498.1, 1398.6), (498.7, 1397.2), (497.0, 1394.6), (497.7, 1393.2), (498.5, 1391.7), (496.9, 1389.0), (500.0, 1389.1), (498.6, 1386.2), (499.5, 1384.9), (500.5, 1383.7), (501.5, 1382.5), (502.5, 1381.2), (503.6, 1380.0), (504.6, 1378.9), (505.8, 1377.8), (507.0, 1376.7), (508.2, 1375.7), (507.7, 1372.5), (509.0, 1371.6), (508.8, 1368.5), (508.6, 1365.3), (511.4, 1366.8), (511.4, 1363.6), (514.1, 1365.2), (516.8, 1366.9), (518.2, 1366.2), (519.7, 1365.5), (521.1, 1364.8), (523.7, 1366.7), (524.1, 1363.6), (525.6, 1363.1), (527.1, 1362.6), (528.6, 1362.1), (530.2, 1361.7), (531.7, 1361.3), (533.2, 1361.0), (534.8, 1360.7), (536.4, 1360.4), (538.0, 1360.2), (539.2, 1357.3), (541.0, 1359.9), (542.4, 1357.1), (544.1, 1359.8), (545.6, 1359.8), (547.3, 1359.9), (548.6, 1362.8), (550.1, 1363.0), (551.7, 1363.3), (553.3, 1363.6), (554.2, 1366.6), (556.4, 1364.3), (557.3, 1367.3), (559.5, 1365.1), (561.0, 1365.5), (561.7, 1368.6), (563.2, 1369.1), (565.6, 1367.2), (567.1, 1367.8), (568.5, 1368.4), (569.9, 1369.1), (571.3, 1369.9), (572.7, 1370.6), (574.1, 1371.5), (575.4, 1372.3), (578.2, 1370.8), (579.6, 1371.7), (580.8, 1372.6), (582.1, 1373.6), (585.0, 1372.5), (586.2, 1373.6), (587.4, 1374.7), (588.5, 1375.8), (587.6, 1378.9), (588.7, 1380.0), (589.8, 1381.2), (590.8, 1382.4), (593.9, 1381.9), (594.9, 1383.2), (598.1, 1382.8), (596.7, 1385.7), (595.4, 1388.6), (596.2, 1389.9), (597.0, 1391.3), (595.3, 1394.0), (596.0, 1395.4), (594.2, 1398.0), (592.3, 1400.5), (592.8, 1402.0), (595.9, 1402.6), (596.4, 1404.1), (596.8, 1405.6), (597.2, 1407.1), (597.6, 1408.7), (600.5, 1409.8), (600.8, 1411.4), (598.3, 1413.3), (601.2, 1414.6), (598.5, 1416.3), (598.6, 1417.9), (598.6, 1419.5), (598.6, 1421.1), (601.3, 1422.7), (601.3, 1424.3), (601.2, 1425.9), (601.0, 1427.5), (600.9, 1429.1), (598.0, 1430.3), (600.4, 1432.3), (597.5, 1433.3), (597.1, 1434.9), (596.7, 1436.4), (598.8, 1438.7), (595.7, 1439.4), (595.2, 1440.9), (597.2, 1443.3), (594.1, 1443.7), (595.9, 1446.3), (592.8, 1446.5), (589.6, 1446.6), (588.8, 1448.0), (590.3, 1450.7), (587.2, 1450.6), (586.3, 1452.0), (587.7, 1454.8), (588.9, 1457.7), (587.9, 1459.0), (589.0, 1462.0), (585.9, 1461.3), (584.8, 1462.5), (585.7, 1465.5), (584.5, 1466.7), (581.5, 1465.7), (578.5, 1464.7), (579.1, 1467.8), (577.8, 1468.8), (574.9, 1467.5), (572.1, 1466.1), (572.2, 1469.3), (570.8, 1470.1), (569.4, 1470.9), (568.0, 1471.6), (566.6, 1472.3), (565.1, 1472.9), (562.6, 1471.0), (561.1, 1471.6), (559.6, 1472.2), (558.1, 1472.7), (556.6, 1473.2), (555.8, 1476.3), (554.3, 1476.7), (552.1, 1474.3), (550.6, 1474.6), (549.5, 1477.6), (547.9, 1477.8), (546.0, 1475.3), (544.7, 1478.1), (543.0, 1478.2), (541.5, 1478.3), (539.8, 1475.6), (538.2, 1475.6), (536.6, 1475.5), (535.0, 1475.4), (533.5, 1475.2), (531.9, 1475.0), (530.7, 1472.1), (528.7, 1474.5), (527.1, 1474.2), (525.6, 1473.9), (524.0, 1473.5), (522.5, 1473.1), (521.7, 1470.0), (520.2, 1469.5), (518.7, 1468.9), (518.2, 1465.8), (515.7, 1467.7), (514.3, 1467.0), (512.9, 1466.3), (510.2, 1468.0), (508.8, 1467.2), (508.8, 1464.1), (506.0, 1465.5), (504.7, 1464.7), (503.4, 1463.8), (502.1, 1462.9), (500.9, 1461.9), (497.9, 1463.0), (496.7, 1462.0), (495.5, 1460.9), (494.3, 1459.9), (493.2, 1458.8), (490.1, 1459.6), (489.0, 1458.4), (487.9, 1457.2), (486.9, 1456.0), (485.8, 1454.8), (487.1, 1451.9), (486.2, 1450.6), (485.3, 1449.3), (484.5, 1447.9), (483.6, 1446.6), (482.9, 1445.2), (482.1, 1443.8), (478.9, 1443.7), (478.2, 1442.3), (475.1, 1441.9), (474.4, 1440.5), (473.8, 1439.0), (473.3, 1437.5), (475.4, 1435.1), (474.9, 1433.6), (471.9, 1432.8), (471.5, 1431.3), (471.1, 1429.7), (470.8, 1428.1), (473.3, 1426.1), (470.4, 1424.9), (467.6, 1423.5), (467.5, 1422.0), (467.4, 1420.4), (467.4, 1418.8), (467.4, 1417.1), (470.3, 1415.7), (470.4, 1414.1), (473.3, 1412.8), (473.5, 1411.3), (476.4, 1410.1), (476.7, 1408.6), (477.1, 1407.0), (477.5, 1405.5), (477.9, 1404.0), (478.3, 1402.4), (478.8, 1400.9), (479.3, 1399.4), (479.9, 1397.9), (480.5, 1396.4), (481.2, 1395.0), (481.8, 1393.5), (482.5, 1392.1), (483.2, 1390.7), (484.0, 1389.3), (484.7, 1387.9), (483.1, 1385.2), (483.9, 1383.8), (484.8, 1382.4), (487.9, 1382.6), (488.8, 1381.3), (489.8, 1380.1), (490.8, 1378.8), (491.8, 1377.6), (492.9, 1376.4), (493.9, 1375.2), (495.0, 1374.1), (494.4, 1371.1), (497.4, 1372.1), (498.6, 1371.1), (501.5, 1372.3), (504.4, 1373.5), (505.7, 1372.6), (507.0, 1371.7), (506.8, 1368.6), (509.6, 1370.1), (509.6, 1366.9), (512.3, 1368.5), (513.7, 1367.7), (515.1, 1367.0), (515.4, 1363.9), (516.8, 1363.2), (518.3, 1362.6), (519.8, 1362.0), (521.3, 1361.4), (523.7, 1363.5), (524.4, 1360.4), (525.9, 1360.0), (527.5, 1359.7), (528.4, 1356.7), (530.0, 1356.4), (532.0, 1358.8), (533.6, 1358.6), (535.2, 1358.4), (537.0, 1360.9), (538.6, 1360.8), (540.1, 1360.8), (541.7, 1358.0), (543.3, 1358.0), (544.9, 1358.0), (546.4, 1358.0), (548.0, 1358.1), (549.7, 1358.3), (550.9, 1361.2), (552.1, 1364.1), (553.6, 1364.4), (555.2, 1364.8), (556.7, 1365.1), (558.3, 1365.4), (559.8, 1365.9), (561.3, 1366.3), (562.8, 1366.8), (565.2, 1364.8), (567.7, 1362.9), (570.2, 1360.9), (571.7, 1361.6), (571.9, 1364.8), (573.3, 1365.5), (576.1, 1363.8), (577.4, 1364.6), (578.7, 1365.5), (581.6, 1364.1), (581.3, 1367.2), (582.6, 1368.1), (583.9, 1369.1), (585.2, 1370.0), (586.4, 1371.0), (585.9, 1374.2), (588.9, 1373.1), (590.0, 1374.2), (589.2, 1377.2), (590.3, 1378.4), (591.4, 1379.5), (592.5, 1380.7), (595.6, 1380.1), (596.7, 1381.3), (595.5, 1384.3), (596.5, 1385.5), (597.5, 1386.8), (600.6, 1386.5), (599.2, 1389.3), (600.0, 1390.7), (600.8, 1392.1), (601.5, 1393.5), (602.3, 1394.9), (602.9, 1396.4), (603.6, 1397.8), (601.8, 1400.4), (604.9, 1400.8), (605.5, 1402.3), (603.4, 1404.7), (606.5, 1405.3), (604.4, 1407.7), (604.8, 1409.2), (605.2, 1410.8), (605.6, 1412.3), (608.6, 1413.4), (606.1, 1415.4), (606.3, 1417.0), (606.6, 1418.6), (609.5, 1419.8), (609.6, 1421.4), (609.7, 1423.0), (609.8, 1424.6), (609.8, 1426.2), (609.9, 1427.8), (612.6, 1429.4), (609.8, 1430.9), (609.7, 1432.5), (606.9, 1433.9), (606.7, 1435.4), (606.5, 1437.0), (606.3, 1438.6), (606.0, 1440.2), (605.7, 1441.7), (605.4, 1443.3), (605.0, 1444.8), (604.6, 1446.4), (604.2, 1447.9), (603.7, 1449.5), (600.6, 1450.1), (600.1, 1451.6), (599.5, 1453.1), (601.5, 1455.6), (600.8, 1457.0), (602.7, 1459.6), (602.0, 1461.0), (601.2, 1462.5), (598.1, 1462.5), (599.7, 1465.3), (596.5, 1465.2), (595.7, 1466.6), (594.8, 1467.9), (593.9, 1469.2), (595.2, 1472.1), (594.3, 1473.4), (591.2, 1473.0), (592.3, 1475.9), (589.2, 1475.4), (588.2, 1476.7), (589.1, 1479.7), (586.0, 1478.9), (584.9, 1480.1), (583.8, 1481.2), (580.8, 1480.2), (579.6, 1481.3), (578.4, 1482.3), (577.2, 1483.3), (575.9, 1484.3), (574.7, 1485.3), (573.4, 1486.2), (572.2, 1487.1), (570.8, 1488.0), (571.0, 1491.2), (568.2, 1489.7), (566.9, 1490.5), (566.9, 1493.7), (565.5, 1494.4), (564.1, 1495.2), (562.6, 1495.9), (561.2, 1496.5), (559.7, 1497.2), (557.2, 1495.3), (555.7, 1495.9), (553.2, 1493.9), (551.8, 1494.5), (551.2, 1497.6), (549.7, 1498.1), (548.2, 1498.5), (546.7, 1499.0), (544.4, 1496.8), (543.6, 1499.8), (542.0, 1500.2), (540.5, 1500.5), (538.9, 1500.8), (537.3, 1501.1), (535.8, 1501.4), (534.2, 1501.6), (532.3, 1499.1), (531.0, 1502.0), (529.4, 1502.2), (527.9, 1502.4), (526.1, 1499.7), (524.5, 1499.8), (523.1, 1502.7), (521.4, 1500.0), (519.8, 1500.0), (518.2, 1500.1), (516.6, 1500.1), (519.0, 1501.4));
outlined(w1000); outline(w1000, cyan); stroke(w1000, 2);
text(wl1000, (540, 1572), "first 1000 primes"); size(wl1000, 20); color(wl1000, dim);
polygon(w5000, (838.4, 1509.2), (838.4, 1509.1), (842.5, 1510.4), (846.4, 1508.8), (850.4, 1509.8), (854.2, 1507.9), (858.3, 1508.6), (862.5, 1509.1), (866.1, 1506.7), (870.3, 1506.9), (872.6, 1501.6), (877.9, 1504.0), (882.0, 1503.6), (884.9, 1500.6), (887.6, 1497.4), (890.1, 1494.1), (894.1, 1493.0), (896.3, 1489.5), (900.2, 1487.9), (903.9, 1486.3), (907.6, 1484.4), (911.1, 1482.2), (914.6, 1479.8), (913.5, 1474.2), (919.1, 1473.0), (919.8, 1468.9), (920.2, 1464.7), (920.5, 1460.6), (923.1, 1457.2), (922.9, 1453.0), (925.1, 1449.5), (924.5, 1445.3), (926.3, 1441.6), (930.6, 1437.9), (929.2, 1433.9), (927.7, 1430.1), (926.0, 1426.3), (926.7, 1422.1), (924.6, 1418.5), (924.9, 1414.2), (924.9, 1410.0), (922.1, 1406.8), (921.7, 1402.7), (921.0, 1398.5), (920.0, 1394.2), (916.5, 1391.8), (915.0, 1387.8), (909.3, 1387.6), (909.4, 1381.9), (905.5, 1380.3), (903.3, 1376.8), (900.9, 1373.4), (896.8, 1372.4), (892.7, 1371.6), (889.6, 1368.7), (883.4, 1373.5), (879.2, 1373.6), (875.1, 1374.0), (871.5, 1371.9), (868.1, 1367.3), (864.3, 1365.7), (860.3, 1367.1), (856.4, 1368.7), (852.0, 1365.1), (847.8, 1364.4), (844.2, 1366.6), (840.7, 1369.1), (837.5, 1371.9), (834.5, 1374.7), (830.3, 1375.4), (826.2, 1376.3), (825.3, 1381.9), (818.0, 1378.8), (814.1, 1380.3), (812.3, 1384.1), (810.7, 1388.0), (807.3, 1390.3), (804.1, 1392.9), (803.4, 1397.0), (800.3, 1399.9), (799.9, 1404.2), (797.2, 1407.4), (797.3, 1411.7), (797.6, 1415.8), (792.9, 1419.0), (791.1, 1422.7), (786.8, 1426.5), (788.3, 1430.4), (792.7, 1433.9), (792.0, 1437.9), (794.3, 1441.3), (794.3, 1445.5), (794.5, 1449.7), (799.9, 1451.5), (803.0, 1454.3), (804.0, 1458.4), (805.2, 1462.5), (808.9, 1464.6), (812.6, 1466.5), (814.6, 1470.2), (820.3, 1469.6), (824.2, 1470.8), (828.2, 1471.8), (830.9, 1474.9), (835.1, 1475.4), (839.4, 1475.7), (841.9, 1480.8), (846.0, 1480.5), (850.1, 1479.8), (853.5, 1484.4), (857.7, 1483.3), (861.7, 1487.4), (865.4, 1483.0), (869.4, 1483.8), (873.7, 1484.4), (876.3, 1479.4), (880.5, 1479.3), (884.7, 1479.1), (888.8, 1478.6), (891.5, 1475.4), (897.2, 1476.5), (899.4, 1472.9), (905.1, 1473.3), (906.8, 1469.4), (908.3, 1465.5), (907.3, 1459.8), (908.2, 1455.7), (913.8, 1454.1), (916.6, 1451.0), (916.7, 1446.8), (916.5, 1442.6), (918.6, 1439.0), (920.4, 1435.2), (919.3, 1431.2), (918.0, 1427.2), (919.3, 1423.2), (920.2, 1419.1), (915.5, 1415.9), (915.8, 1411.8), (913.2, 1408.5), (915.4, 1403.2), (914.8, 1399.0), (916.1, 1393.4), (914.9, 1389.5), (909.2, 1389.0), (907.5, 1385.1), (905.6, 1381.3), (900.0, 1381.9), (897.6, 1378.5), (893.5, 1377.6), (889.3, 1376.9), (886.3, 1373.9), (881.1, 1376.4), (877.8, 1373.9), (873.6, 1374.3), (870.1, 1372.2), (866.7, 1367.6), (862.7, 1366.2), (858.6, 1365.1), (855.2, 1369.6), (851.1, 1369.0), (846.8, 1368.7), (843.5, 1371.3), (839.2, 1371.6), (836.2, 1374.6), (830.8, 1372.9), (828.2, 1376.2), (827.3, 1381.8), (825.2, 1385.5), (823.5, 1389.2), (819.9, 1391.4), (818.7, 1395.4), (817.9, 1399.5), (817.2, 1403.6), (814.3, 1406.8), (814.3, 1411.0), (811.9, 1414.4), (809.7, 1418.0), (807.7, 1421.8), (808.8, 1425.8), (807.3, 1429.6), (806.1, 1433.5), (805.2, 1437.6), (801.9, 1442.3), (804.4, 1445.8), (807.0, 1449.0), (807.3, 1453.1), (810.4, 1455.9), (816.0, 1456.9), (817.5, 1460.8), (819.3, 1464.6), (821.2, 1468.3), (823.3, 1471.9), (825.7, 1475.4), (829.7, 1476.5), (833.8, 1477.2), (837.8, 1477.7), (842.0, 1478.0), (844.6, 1483.1), (848.1, 1485.3), (851.5, 1490.0), (855.6, 1489.0), (859.6, 1484.9), (863.7, 1485.9), (867.8, 1486.6), (871.8, 1487.1), (875.2, 1484.6), (879.4, 1484.5), (882.5, 1481.6), (884.0, 1476.1), (888.1, 1475.0), (890.5, 1471.5), (892.6, 1467.8), (894.2, 1464.0), (897.7, 1461.7), (898.9, 1457.7), (899.7, 1453.5), (902.8, 1450.7), (900.7, 1445.3), (903.4, 1442.2), (905.8, 1438.7), (908.1, 1435.2), (904.6, 1430.8), (909.0, 1427.1), (907.7, 1423.1), (906.1, 1419.2), (901.5, 1416.0), (896.6, 1413.0), (894.2, 1409.7), (899.3, 1403.7), (896.5, 1400.6), (895.8, 1396.6), (894.8, 1392.4), (891.3, 1389.9), (887.8, 1387.7), (888.1, 1382.0), (888.1, 1376.3), (886.0, 1372.8), (881.9, 1371.7), (876.4, 1373.2), (872.3, 1372.7), (869.1, 1370.0), (864.9, 1370.0), (861.4, 1367.6), (857.2, 1368.2), (853.0, 1369.0), (849.0, 1370.2), (844.8, 1369.1), (841.0, 1371.0), (836.9, 1370.4), (832.7, 1370.1), (827.8, 1367.4), (822.6, 1365.2), (818.4, 1365.8), (814.2, 1366.6), (811.6, 1370.1), (809.5, 1373.7), (805.7, 1375.4), (804.1, 1379.3), (798.4, 1380.1), (799.8, 1385.6), (796.7, 1388.5), (796.2, 1392.8), (796.1, 1397.1), (793.8, 1400.7), (794.4, 1404.9), (795.3, 1409.1), (796.5, 1413.1), (795.4, 1417.0), (794.5, 1421.1), (799.3, 1424.2), (799.1, 1428.4), (801.7, 1431.6), (802.1, 1435.7), (807.6, 1437.3), (808.7, 1441.4), (809.9, 1445.2), (809.6, 1450.9), (815.3, 1450.8), (819.2, 1452.1), (820.2, 1457.8), (824.4, 1458.4), (828.6, 1458.8), (831.1, 1464.0), (835.3, 1463.7), (839.5, 1463.2), (842.9, 1467.8), (847.0, 1466.7), (850.9, 1468.1), (854.9, 1466.4), (858.6, 1464.5), (863.3, 1467.7), (867.4, 1467.8), (871.6, 1467.7), (875.7, 1467.3), (879.9, 1466.5), (882.4, 1463.1), (884.7, 1459.5), (886.7, 1455.8), (890.4, 1454.0), (892.0, 1450.2), (891.1, 1444.6), (892.1, 1440.6), (900.1, 1440.6), (898.1, 1435.3), (898.2, 1431.1), (898.0, 1426.9), (897.5, 1422.6), (899.4, 1418.8), (901.0, 1415.0), (902.4, 1411.1), (903.5, 1406.9), (901.6, 1403.1), (902.1, 1399.0), (899.7, 1395.6), (899.6, 1391.4), (894.2, 1389.8), (893.4, 1385.7), (890.1, 1383.3), (888.6, 1379.3), (888.9, 1373.6), (885.0, 1372.0), (882.6, 1368.5), (878.4, 1367.6), (874.2, 1366.9), (870.1, 1366.5), (867.9, 1361.2), (863.7, 1361.4), (860.2, 1359.2), (856.9, 1354.5), (852.8, 1355.5), (848.9, 1357.0), (844.6, 1353.2), (841.6, 1358.0), (837.5, 1357.7), (833.3, 1357.8), (830.4, 1360.9), (826.2, 1361.8), (825.5, 1367.5), (823.3, 1371.1), (821.3, 1374.9), (817.7, 1377.0), (816.3, 1380.9), (810.7, 1382.0), (807.5, 1384.8), (807.1, 1388.9), (801.8, 1391.0), (799.3, 1394.3), (799.6, 1398.5), (797.5, 1402.1), (798.4, 1406.1), (796.9, 1409.8), (798.5, 1413.6), (795.0, 1418.1), (794.5, 1422.3), (796.9, 1425.6), (799.6, 1428.7), (800.0, 1432.9), (805.5, 1434.5), (808.7, 1437.1), (809.8, 1441.2), (813.3, 1443.4), (813.0, 1449.0), (816.9, 1450.6), (820.8, 1451.9), (823.4, 1455.2), (828.9, 1453.6), (831.8, 1456.5), (834.1, 1461.8), (836.6, 1467.0), (841.5, 1463.9), (846.0, 1460.5), (849.9, 1462.2), (853.9, 1460.8), (857.7, 1459.1), (862.2, 1462.6), (865.7, 1460.4), (870.8, 1463.1), (873.9, 1460.3), (876.8, 1457.3), (879.3, 1454.0), (883.3, 1452.6), (885.4, 1448.9), (891.1, 1448.9), (890.3, 1443.3), (895.9, 1442.3), (899.1, 1439.5), (897.1, 1434.2), (899.8, 1430.8), (902.1, 1427.2), (904.2, 1423.6), (906.0, 1419.7), (902.0, 1415.6), (903.2, 1411.6), (898.7, 1407.9), (899.3, 1403.8), (899.6, 1399.7), (896.9, 1396.4), (891.5, 1394.7), (893.1, 1389.2), (887.4, 1388.2), (884.0, 1385.9), (880.3, 1383.8), (876.6, 1382.0), (872.7, 1380.5), (870.3, 1377.0), (867.6, 1373.7), (863.5, 1373.1), (859.2, 1372.9), (855.8, 1370.4), (851.0, 1373.6), (847.3, 1371.7), (843.2, 1372.8), (839.3, 1374.1), (835.2, 1373.0), (831.0, 1372.2), (827.4, 1374.4), (824.0, 1376.7), (821.7, 1382.0), (816.4, 1379.8), (814.7, 1385.3), (809.2, 1383.9), (806.8, 1387.3), (806.5, 1393.0), (804.6, 1396.7), (803.0, 1400.5), (799.7, 1402.9), (798.9, 1407.1), (791.1, 1407.9), (791.0, 1412.1), (786.1, 1415.0), (784.3, 1418.8), (785.5, 1422.7), (784.4, 1426.6), (783.6, 1430.8), (780.6, 1435.7), (785.8, 1438.1), (791.1, 1440.3), (791.7, 1444.5), (795.0, 1447.0), (796.3, 1451.0), (799.8, 1453.1), (801.4, 1457.0), (809.0, 1454.9), (809.4, 1460.6), (813.3, 1461.8), (817.4, 1462.4), (819.4, 1467.8), (823.6, 1468.0), (828.6, 1465.1), (832.8, 1464.6), (836.3, 1469.1), (840.1, 1473.4), (844.0, 1472.0), (847.9, 1470.3), (851.2, 1465.6), (855.3, 1466.1), (859.6, 1466.2), (864.8, 1468.6), (867.8, 1465.5), (870.4, 1462.3), (874.5, 1461.2), (876.8, 1457.7), (880.5, 1455.9), (882.2, 1452.0), (881.3, 1446.3), (886.9, 1444.8), (884.8, 1439.5), (890.1, 1437.4), (892.6, 1433.9), (894.7, 1430.2), (896.6, 1426.4), (895.4, 1422.4), (894.0, 1418.4), (892.3, 1414.6), (893.0, 1410.7), (890.7, 1407.2), (888.3, 1403.9), (888.1, 1399.9), (885.0, 1396.9), (886.3, 1391.4), (885.0, 1387.4), (881.3, 1385.3), (877.5, 1383.5), (873.5, 1381.9), (871.2, 1378.4), (868.6, 1374.9), (864.4, 1374.1), (860.3, 1373.8), (856.1, 1373.7), (852.7, 1371.4), (849.1, 1369.2), (845.5, 1367.4), (841.5, 1371.4), (837.5, 1370.2), (833.4, 1369.3), (829.7, 1371.2), (826.2, 1373.5), (822.6, 1376.0), (817.4, 1373.8), (814.4, 1376.8), (810.4, 1377.7), (804.9, 1376.8), (802.8, 1380.5), (801.1, 1384.3), (799.6, 1388.4), (796.3, 1391.0), (793.1, 1393.9), (790.1, 1396.9), (787.4, 1400.1), (784.9, 1403.5), (780.0, 1406.5), (775.4, 1409.9), (773.9, 1413.8), (775.4, 1417.8), (777.1, 1421.6), (773.5, 1426.1), (775.7, 1429.6), (778.2, 1433.0), (783.6, 1435.2), (784.2, 1439.3), (780.4, 1446.3), (781.7, 1450.3), (785.5, 1452.2), (787.6, 1455.8), (793.2, 1455.0), (795.6, 1458.3), (798.3, 1461.4), (802.4, 1461.9), (806.5, 1462.0), (809.0, 1467.1), (813.2, 1466.7), (817.3, 1466.0), (821.7, 1462.3), (825.7, 1461.0), (829.5, 1459.4), (833.6, 1460.3), (837.3, 1458.3), (840.3, 1453.4), (844.4, 1453.5), (848.7, 1453.2), (854.2, 1454.7), (856.8, 1451.2), (858.9, 1447.7), (860.7, 1444.0), (864.3, 1442.0), (869.9, 1441.0), (872.9, 1438.3), (873.3, 1434.0), (873.2, 1429.7), (870.1, 1424.9), (869.3, 1420.7), (870.9, 1416.8), (872.2, 1412.8), (873.1, 1408.7), (871.0, 1405.1), (868.7, 1401.6), (863.4, 1399.3), (863.1, 1395.2), (860.0, 1392.4), (861.2, 1386.8), (857.7, 1384.6), (856.1, 1380.7), (854.0, 1377.1), (851.6, 1373.6), (846.0, 1374.8), (843.2, 1371.7), (839.2, 1371.4), (835.8, 1368.7), (831.5, 1368.9), (827.3, 1369.6), (823.5, 1367.8), (819.4, 1371.8), (815.5, 1373.3), (811.7, 1375.2), (808.0, 1377.4), (803.1, 1374.6), (799.9, 1377.3), (795.7, 1377.7), (791.6, 1378.5), (786.0, 1377.3), (783.8, 1380.8), (781.8, 1384.5), (778.2, 1386.5), (770.3, 1385.7), (769.5, 1389.9), (764.0, 1391.5), (761.1, 1394.5), (755.8, 1396.9), (756.1, 1401.1), (754.0, 1404.7), (752.1, 1408.5), (750.5, 1412.4), (749.3, 1416.3), (751.1, 1420.1), (755.8, 1423.3), (752.9, 1428.1), (758.2, 1430.1), (758.8, 1434.4), (764.5, 1435.5), (766.0, 1439.6), (769.8, 1441.6), (771.8, 1445.2), (774.2, 1448.6), (776.9, 1451.7), (781.1, 1452.2), (784.2, 1454.9), (787.4, 1457.5), (790.8, 1459.9), (794.9, 1459.4), (798.7, 1461.1), (802.7, 1459.9), (806.6, 1461.1), (810.1, 1456.6), (813.8, 1454.5), (818.0, 1454.6), (822.2, 1454.3), (826.4, 1453.7), (828.9, 1450.4), (832.9, 1449.0), (836.8, 1447.2), (840.5, 1445.2), (841.9, 1441.2), (843.2, 1437.1), (846.5, 1434.5), (844.9, 1429.0), (845.4, 1425.0), (842.9, 1419.9), (842.8, 1415.7), (842.3, 1411.6), (838.7, 1407.2), (842.9, 1403.5), (844.1, 1399.5), (844.8, 1395.3), (842.5, 1391.8), (840.0, 1388.3), (837.3, 1385.0), (834.3, 1382.0), (835.8, 1376.5), (832.4, 1374.1), (831.1, 1370.2), (829.4, 1366.5), (829.1, 1360.8), (821.8, 1364.1), (819.2, 1360.9), (817.1, 1355.4), (813.8, 1352.8), (809.6, 1353.2), (806.0, 1351.2), (802.0, 1355.1), (797.7, 1351.2), (794.1, 1353.1), (789.9, 1352.5), (787.2, 1357.5), (784.0, 1360.0), (780.9, 1362.9), (778.0, 1366.1), (775.6, 1369.4), (771.7, 1370.9), (767.9, 1372.7), (766.4, 1376.6), (767.6, 1382.1), (764.4, 1385.0), (763.8, 1389.2), (763.7, 1393.5), (764.0, 1397.6), (764.5, 1401.8), (762.6, 1405.3), (758.3, 1409.1), (759.8, 1412.8), (761.7, 1416.7), (761.2, 1420.9), (766.2, 1423.6), (768.8, 1426.9), (769.0, 1431.1), (767.1, 1436.6), (772.6, 1437.9), (773.8, 1442.0), (775.6, 1445.8), (779.4, 1447.4), (781.7, 1451.0), (784.2, 1454.3), (788.4, 1454.9), (792.6, 1455.3), (795.1, 1460.5), (798.7, 1462.7), (802.8, 1461.7), (806.9, 1465.8), (811.0, 1466.9), (814.7, 1465.0), (818.9, 1465.5), (822.3, 1463.0), (825.4, 1460.3), (830.7, 1462.3), (833.4, 1459.1), (836.0, 1455.8), (840.0, 1454.6), (841.9, 1451.1), (843.8, 1447.2), (845.3, 1443.3), (851.0, 1442.6), (852.0, 1438.6), (854.9, 1435.7), (855.2, 1431.4), (857.7, 1428.0), (854.6, 1423.3), (856.4, 1419.4), (855.3, 1415.4), (859.4, 1411.5), (860.5, 1407.4), (861.2, 1403.2), (864.3, 1398.4), (861.8, 1395.0), (856.4, 1393.0), (855.7, 1389.0), (857.0, 1383.4), (851.3, 1382.7), (847.7, 1380.7), (845.7, 1376.9), (843.5, 1373.3), (839.5, 1372.1), (837.0, 1368.8), (834.3, 1365.8), (829.0, 1368.0), (826.7, 1362.7), (823.2, 1360.3), (819.6, 1358.2), (815.9, 1356.4), (811.6, 1357.8), (808.1, 1362.2), (804.5, 1364.5), (801.9, 1369.6), (797.7, 1369.8), (794.6, 1372.7), (790.5, 1373.5), (787.9, 1376.8), (782.3, 1376.0), (778.4, 1377.8), (778.7, 1383.5), (777.2, 1387.5), (771.5, 1388.5), (768.4, 1391.3), (765.4, 1394.4), (762.7, 1397.6), (762.9, 1401.9), (760.8, 1405.5), (756.3, 1409.0), (757.7, 1413.2), (759.5, 1417.1), (758.8, 1421.3), (755.7, 1426.0), (758.2, 1429.1), (758.7, 1433.3), (761.8, 1436.2), (762.9, 1440.2), (766.5, 1442.5), (772.3, 1442.6), (776.2, 1444.3), (778.4, 1447.8), (782.2, 1448.7), (784.3, 1454.0), (788.4, 1453.9), (792.5, 1453.5), (796.6, 1452.9), (800.3, 1457.3), (804.3, 1456.0), (808.1, 1454.4), (811.9, 1452.5), (816.1, 1453.0), (819.4, 1450.5), (823.6, 1450.2), (826.4, 1447.1), (830.4, 1446.2), (832.5, 1442.7), (836.4, 1440.9), (838.0, 1437.1), (839.4, 1433.1), (840.5, 1429.1), (845.9, 1427.7), (843.9, 1422.4), (849.2, 1420.2), (851.7, 1416.8), (851.2, 1412.7), (850.5, 1408.7), (852.2, 1404.9), (848.2, 1400.8), (846.6, 1397.0), (844.6, 1393.4), (842.3, 1390.0), (842.3, 1385.9), (841.8, 1381.8), (838.7, 1379.1), (837.6, 1375.0), (834.1, 1372.8), (828.4, 1372.9), (828.2, 1367.2), (822.6, 1367.9), (820.1, 1364.3), (816.0, 1363.5), (810.7, 1365.4), (807.4, 1362.9), (803.8, 1360.6), (799.7, 1361.4), (795.7, 1362.6), (791.8, 1364.1), (788.5, 1368.7), (784.2, 1368.4), (779.1, 1365.8), (775.0, 1366.1), (772.0, 1369.1), (768.0, 1370.0), (765.7, 1373.5), (759.9, 1373.3), (758.6, 1377.2), (755.2, 1379.7), (754.4, 1383.7), (751.4, 1386.6), (748.6, 1389.7), (751.3, 1394.7), (751.6, 1399.0), (747.0, 1402.2), (750.8, 1406.4), (749.4, 1410.5), (751.2, 1414.2), (750.6, 1418.3), (755.6, 1421.2), (755.6, 1425.4), (755.7, 1429.6), (756.4, 1433.7), (757.3, 1437.8), (760.8, 1440.3), (764.4, 1442.3), (766.2, 1446.0), (766.7, 1451.6), (770.8, 1452.5), (774.9, 1453.1), (779.1, 1453.4), (781.7, 1458.5), (785.9, 1458.2), (789.5, 1460.3), (793.5, 1462.1), (797.4, 1463.5), (801.1, 1461.8), (804.3, 1457.0), (808.6, 1457.4), (811.1, 1452.3), (816.4, 1454.5), (820.4, 1453.7), (822.8, 1450.1), (824.8, 1446.3), (826.5, 1442.6), (830.1, 1440.3), (828.9, 1434.7), (831.9, 1431.6), (834.7, 1428.4), (834.5, 1424.2), (836.7, 1420.7), (833.3, 1416.2), (835.0, 1412.2), (830.7, 1408.3), (831.7, 1404.2), (829.6, 1400.6), (827.2, 1397.0), (827.3, 1392.9), (824.5, 1389.7), (819.0, 1387.8), (820.3, 1382.2), (816.8, 1379.9), (813.1, 1377.9), (811.2, 1374.3), (810.6, 1368.6), (808.0, 1365.2), (803.9, 1364.3), (801.1, 1361.4), (798.7, 1356.2), (795.3, 1353.8), (791.1, 1354.4), (787.1, 1355.3), (783.1, 1353.7), (779.2, 1355.2), (775.4, 1356.9), (770.9, 1353.4), (768.2, 1358.5), (764.0, 1358.5), (758.9, 1356.2), (754.8, 1356.8), (753.7, 1362.4), (752.9, 1368.1), (750.7, 1371.6), (746.9, 1373.3), (741.2, 1373.4), (739.8, 1377.4), (738.9, 1381.5), (735.9, 1384.4), (733.0, 1387.5), (732.9, 1391.6), (730.5, 1395.0), (731.1, 1399.2), (729.3, 1402.9), (733.3, 1407.1), (729.4, 1411.2), (731.3, 1414.9), (733.6, 1418.2), (738.8, 1420.5), (736.9, 1425.9), (738.0, 1430.0), (739.4, 1433.9), (737.2, 1441.5), (741.1, 1443.0), (745.1, 1444.2), (749.3, 1445.2), (753.5, 1445.8), (758.7, 1443.3), (762.1, 1445.8), (766.3, 1445.1), (770.3, 1444.0), (774.1, 1445.4), (777.9, 1443.9), (781.3, 1439.3), (785.5, 1439.9), (790.4, 1442.7), (793.7, 1440.0), (797.9, 1439.6), (800.8, 1436.4), (803.1, 1433.0), (806.9, 1431.3), (812.6, 1431.2), (816.1, 1429.1), (817.1, 1425.0), (820.3, 1422.3), (825.7, 1420.4), (831.0, 1418.1), (830.8, 1413.9), (830.2, 1409.8), (832.0, 1406.0), (830.8, 1402.0), (829.2, 1397.9), (829.8, 1393.9), (830.2, 1389.7), (830.2, 1385.7), (829.7, 1381.7), (828.9, 1377.5), (825.6, 1374.9), (820.0, 1374.6), (820.0, 1368.9), (817.7, 1365.3), (813.7, 1364.1), (809.6, 1363.4), (805.5, 1363.0), (802.2, 1360.5), (798.1, 1360.8), (795.1, 1356.1), (791.2, 1354.5), (787.4, 1356.0), (783.4, 1355.2), (780.0, 1357.3), (775.7, 1357.2), (772.5, 1359.9), (769.5, 1363.0), (766.9, 1366.2), (766.3, 1371.9), (764.4, 1375.5), (760.7, 1377.3), (759.4, 1381.3), (756.1, 1383.7), (755.4, 1387.8), (752.5, 1390.7), (749.9, 1393.7), (747.5, 1397.1), (745.3, 1400.8), (746.3, 1404.6), (745.2, 1408.6), (744.4, 1412.8), (743.9, 1416.9), (746.4, 1420.4), (749.2, 1423.4), (749.8, 1427.6), (748.4, 1433.1), (751.9, 1435.5), (755.4, 1437.4), (757.3, 1441.1), (762.9, 1440.4), (766.9, 1441.5), (769.6, 1444.8), (772.5, 1447.7), (776.7, 1448.0), (781.0, 1447.9), (785.0, 1447.4), (788.8, 1449.2), (792.7, 1450.9), (796.6, 1454.8), (800.1, 1450.2), (803.6, 1447.9), (808.0, 1447.9), (811.1, 1444.9), (815.3, 1444.1), (819.2, 1443.0), (824.9, 1443.6), (828.7, 1441.9), (832.4, 1439.8), (835.9, 1437.5), (832.4, 1439.8), (828.7, 1441.9), (824.9, 1443.6), (819.2, 1443.0), (815.3, 1444.1), (811.1, 1444.9), (808.0, 1447.9), (803.6, 1447.9), (800.1, 1450.2), (796.6, 1454.8), (792.7, 1450.9), (788.8, 1449.2), (785.0, 1447.4), (781.0, 1447.9), (776.7, 1448.0), (772.5, 1447.7), (769.6, 1444.8), (766.9, 1441.5), (762.9, 1440.4), (757.3, 1441.1), (755.4, 1437.4), (751.9, 1435.5), (748.4, 1433.1), (749.8, 1427.6), (749.2, 1423.4), (746.4, 1420.4), (743.9, 1416.9), (744.4, 1412.8), (745.2, 1408.6), (746.3, 1404.6), (745.3, 1400.8), (747.5, 1397.1), (749.9, 1393.7), (752.5, 1390.7), (755.4, 1387.8), (756.1, 1383.7), (759.4, 1381.3), (760.7, 1377.3), (764.4, 1375.5), (766.3, 1371.9), (766.9, 1366.2), (769.5, 1363.0), (772.5, 1359.9), (775.7, 1357.2), (780.0, 1357.3), (783.4, 1355.2), (787.4, 1356.0), (791.2, 1354.5), (795.1, 1356.1), (798.1, 1360.8), (802.2, 1360.5), (805.5, 1363.0), (809.6, 1363.4), (813.7, 1364.1), (817.7, 1365.3), (820.0, 1368.9), (820.0, 1374.6), (825.6, 1374.9), (828.9, 1377.5), (829.7, 1381.7), (830.2, 1385.7), (830.2, 1389.7), (829.8, 1393.9), (829.2, 1397.9), (830.8, 1402.0), (832.0, 1406.0), (830.2, 1409.8), (830.8, 1413.9), (831.0, 1418.1), (825.7, 1420.4), (820.3, 1422.3), (817.1, 1425.0), (816.1, 1429.1), (812.6, 1431.2), (806.9, 1431.3), (803.1, 1433.0), (800.8, 1436.4), (797.9, 1439.6), (793.7, 1440.0), (790.4, 1442.7), (785.5, 1439.9), (781.3, 1439.3), (777.9, 1443.9), (774.1, 1445.4), (770.3, 1444.0), (766.3, 1445.1), (762.1, 1445.8), (758.7, 1443.3), (753.5, 1445.8), (749.3, 1445.2), (745.1, 1444.2), (741.1, 1443.0), (737.2, 1441.5), (739.4, 1433.9), (738.0, 1430.0), (736.9, 1425.9), (738.8, 1420.5), (733.6, 1418.2), (731.3, 1414.9), (729.4, 1411.2), (733.3, 1407.1), (729.3, 1402.9), (731.1, 1399.2), (730.5, 1395.0), (732.9, 1391.6), (733.0, 1387.5), (735.9, 1384.4), (738.9, 1381.5), (739.8, 1377.4), (741.2, 1373.4), (746.9, 1373.3), (750.7, 1371.6), (752.9, 1368.1), (753.7, 1362.4), (754.8, 1356.8), (758.9, 1356.2), (764.0, 1358.5), (768.2, 1358.5), (770.9, 1353.4), (775.4, 1356.9), (779.2, 1355.2), (783.1, 1353.7), (787.1, 1355.3), (791.1, 1354.4), (795.3, 1353.8), (798.7, 1356.2), (801.1, 1361.4), (803.9, 1364.3), (808.0, 1365.2), (810.6, 1368.6), (811.2, 1374.3), (813.1, 1377.9), (816.8, 1379.9), (820.3, 1382.2), (819.0, 1387.8), (824.5, 1389.7), (827.3, 1392.9), (827.2, 1397.0), (829.6, 1400.6), (831.7, 1404.2), (830.7, 1408.3), (835.0, 1412.2), (833.3, 1416.2), (836.7, 1420.7), (834.5, 1424.2), (834.7, 1428.4), (831.9, 1431.6), (828.9, 1434.7), (830.1, 1440.3), (826.5, 1442.6), (824.8, 1446.3), (822.8, 1450.1), (820.4, 1453.7), (816.4, 1454.5), (811.1, 1452.3), (808.6, 1457.4), (804.3, 1457.0), (801.1, 1461.8), (797.4, 1463.5), (793.5, 1462.1), (789.5, 1460.3), (785.9, 1458.2), (781.7, 1458.5), (779.1, 1453.4), (774.9, 1453.1), (770.8, 1452.5), (766.7, 1451.6), (766.2, 1446.0), (764.4, 1442.3), (760.8, 1440.3), (757.3, 1437.8), (756.4, 1433.7), (755.7, 1429.6), (755.6, 1425.4), (755.6, 1421.2), (750.6, 1418.3), (751.2, 1414.2), (749.4, 1410.5), (750.8, 1406.4), (747.0, 1402.2), (751.6, 1399.0), (751.3, 1394.7), (748.6, 1389.7), (751.4, 1386.6), (754.4, 1383.7), (755.2, 1379.7), (758.6, 1377.2), (759.9, 1373.3), (765.7, 1373.5), (768.0, 1370.0), (772.0, 1369.1), (775.0, 1366.1), (779.1, 1365.8), (784.2, 1368.4), (788.5, 1368.7), (791.8, 1364.1), (795.7, 1362.6), (799.7, 1361.4), (803.8, 1360.6), (807.4, 1362.9), (810.7, 1365.4), (816.0, 1363.5), (820.1, 1364.3), (822.6, 1367.9), (828.2, 1367.2), (828.4, 1372.9), (834.1, 1372.8), (837.6, 1375.0), (838.7, 1379.1), (841.8, 1381.8), (842.3, 1385.9), (842.3, 1390.0), (844.6, 1393.4), (846.6, 1397.0), (848.2, 1400.8), (852.2, 1404.9), (850.5, 1408.7), (851.2, 1412.7), (851.7, 1416.8), (849.2, 1420.2), (843.9, 1422.4), (845.9, 1427.7), (840.5, 1429.1), (839.4, 1433.1), (838.0, 1437.1), (836.4, 1440.9), (832.5, 1442.7), (830.4, 1446.2), (826.4, 1447.1), (823.6, 1450.2), (819.4, 1450.5), (816.1, 1453.0), (811.9, 1452.5), (808.1, 1454.4), (804.3, 1456.0), (800.3, 1457.3), (796.6, 1452.9), (792.5, 1453.5), (788.4, 1453.9), (784.3, 1454.0), (782.2, 1448.7), (778.4, 1447.8), (776.2, 1444.3), (772.3, 1442.6), (766.5, 1442.5), (762.9, 1440.2), (761.8, 1436.2), (758.7, 1433.3), (758.2, 1429.1), (755.7, 1426.0), (758.8, 1421.3), (759.5, 1417.1), (757.7, 1413.2), (756.3, 1409.0), (760.8, 1405.5), (762.9, 1401.9), (762.7, 1397.6), (765.4, 1394.4), (768.4, 1391.3), (771.5, 1388.5), (777.2, 1387.5), (778.7, 1383.5), (778.4, 1377.8), (782.3, 1376.0), (787.9, 1376.8), (790.5, 1373.5), (794.6, 1372.7), (797.7, 1369.8), (801.9, 1369.6), (804.5, 1364.5), (808.1, 1362.2), (811.6, 1357.8), (815.9, 1356.4), (819.6, 1358.2), (823.2, 1360.3), (826.7, 1362.7), (829.0, 1368.0), (834.3, 1365.8), (837.0, 1368.8), (839.5, 1372.1), (843.5, 1373.3), (845.7, 1376.9), (847.7, 1380.7), (851.3, 1382.7), (857.0, 1383.4), (855.7, 1389.0), (856.4, 1393.0), (861.8, 1395.0), (864.3, 1398.4), (861.2, 1403.2), (860.5, 1407.4), (859.4, 1411.5), (855.3, 1415.4), (856.4, 1419.4), (854.6, 1423.3), (857.7, 1428.0), (855.2, 1431.4), (854.9, 1435.7), (852.0, 1438.6), (851.0, 1442.6), (845.3, 1443.3), (843.8, 1447.2), (841.9, 1451.1), (840.0, 1454.6), (836.0, 1455.8), (833.4, 1459.1), (830.7, 1462.3), (825.4, 1460.3), (822.3, 1463.0), (818.9, 1465.5), (814.7, 1465.0), (811.0, 1466.9), (806.9, 1465.8), (802.8, 1461.7), (798.7, 1462.7), (795.1, 1460.5), (792.6, 1455.3), (788.4, 1454.9), (784.2, 1454.3), (781.7, 1451.0), (779.4, 1447.4), (775.6, 1445.8), (773.8, 1442.0), (772.6, 1437.9), (767.1, 1436.6), (769.0, 1431.1), (768.8, 1426.9), (766.2, 1423.6), (761.2, 1420.9), (761.7, 1416.7), (759.8, 1412.8), (758.3, 1409.1), (762.6, 1405.3), (764.5, 1401.8), (764.0, 1397.6), (763.7, 1393.5), (763.8, 1389.2), (764.4, 1385.0), (767.6, 1382.1), (766.4, 1376.6), (767.9, 1372.7), (771.7, 1370.9), (775.6, 1369.4), (778.0, 1366.1), (780.9, 1362.9), (784.0, 1360.0), (787.2, 1357.5), (789.9, 1352.5), (794.1, 1353.1), (797.7, 1351.2), (802.0, 1355.1), (806.0, 1351.2), (809.6, 1353.2), (813.8, 1352.8), (817.1, 1355.4), (819.2, 1360.9), (821.8, 1364.1), (829.1, 1360.8), (829.4, 1366.5), (831.1, 1370.2), (832.4, 1374.1), (835.8, 1376.5), (834.3, 1382.0), (837.3, 1385.0), (840.0, 1388.3), (842.5, 1391.8), (844.8, 1395.3), (844.1, 1399.5), (842.9, 1403.5), (838.7, 1407.2), (842.3, 1411.6), (842.8, 1415.7), (842.9, 1419.9), (845.4, 1425.0), (844.9, 1429.0), (846.5, 1434.5), (843.2, 1437.1), (841.9, 1441.2), (840.5, 1445.2), (836.8, 1447.2), (832.9, 1449.0), (828.9, 1450.4), (826.4, 1453.7), (822.2, 1454.3), (818.0, 1454.6), (813.8, 1454.5), (810.1, 1456.6), (806.6, 1461.1), (802.7, 1459.9), (798.7, 1461.1), (794.9, 1459.4), (790.8, 1459.9), (787.4, 1457.5), (784.2, 1454.9), (781.1, 1452.2), (776.9, 1451.7), (774.2, 1448.6), (771.8, 1445.2), (769.8, 1441.6), (766.0, 1439.6), (764.5, 1435.5), (758.8, 1434.4), (758.2, 1430.1), (752.9, 1428.1), (755.8, 1423.3), (751.1, 1420.1), (749.3, 1416.3), (750.5, 1412.4), (752.1, 1408.5), (754.0, 1404.7), (756.1, 1401.1), (755.8, 1396.9), (761.1, 1394.5), (764.0, 1391.5), (769.5, 1389.9), (770.3, 1385.7), (778.2, 1386.5), (781.8, 1384.5), (783.8, 1380.8), (786.0, 1377.3), (791.6, 1378.5), (795.7, 1377.7), (799.9, 1377.3), (803.1, 1374.6), (808.0, 1377.4), (811.7, 1375.2), (815.5, 1373.3), (819.4, 1371.8), (823.5, 1367.8), (827.3, 1369.6), (831.5, 1368.9), (835.8, 1368.7), (839.2, 1371.4), (843.2, 1371.7), (846.0, 1374.8), (851.6, 1373.6), (854.0, 1377.1), (856.1, 1380.7), (857.7, 1384.6), (861.2, 1386.8), (860.0, 1392.4), (863.1, 1395.2), (863.4, 1399.3), (868.7, 1401.6), (871.0, 1405.1), (873.1, 1408.7), (872.2, 1412.8), (870.9, 1416.8), (869.3, 1420.7), (870.1, 1424.9), (873.2, 1429.7), (873.3, 1434.0), (872.9, 1438.3), (869.9, 1441.0), (864.3, 1442.0), (860.7, 1444.0), (858.9, 1447.7), (856.8, 1451.2), (854.2, 1454.7), (848.7, 1453.2), (844.4, 1453.5), (840.3, 1453.4), (837.3, 1458.3), (833.6, 1460.3), (829.5, 1459.4), (825.7, 1461.0), (821.7, 1462.3), (817.3, 1466.0), (813.2, 1466.7), (809.0, 1467.1), (806.5, 1462.0), (802.4, 1461.9), (798.3, 1461.4), (795.6, 1458.3), (793.2, 1455.0), (787.6, 1455.8), (785.5, 1452.2), (781.7, 1450.3), (780.4, 1446.3), (784.2, 1439.3), (783.6, 1435.2), (778.2, 1433.0), (775.7, 1429.6), (773.5, 1426.1), (777.1, 1421.6), (775.4, 1417.8), (773.9, 1413.8), (775.4, 1409.9), (780.0, 1406.5), (784.9, 1403.5), (787.4, 1400.1), (790.1, 1396.9), (793.1, 1393.9), (796.3, 1391.0), (799.6, 1388.4), (801.1, 1384.3), (802.8, 1380.5), (804.9, 1376.8), (810.4, 1377.7), (814.4, 1376.8), (817.4, 1373.8), (822.6, 1376.0), (826.2, 1373.5), (829.7, 1371.2), (833.4, 1369.3), (837.5, 1370.2), (841.5, 1371.4), (845.5, 1367.4), (849.1, 1369.2), (852.7, 1371.4), (856.1, 1373.7), (860.3, 1373.8), (864.4, 1374.1), (868.6, 1374.9), (871.2, 1378.4), (873.5, 1381.9), (877.5, 1383.5), (881.3, 1385.3), (885.0, 1387.4), (886.3, 1391.4), (885.0, 1396.9), (888.1, 1399.9), (888.3, 1403.9), (890.7, 1407.2), (893.0, 1410.7), (892.3, 1414.6), (894.0, 1418.4), (895.4, 1422.4), (896.6, 1426.4), (894.7, 1430.2), (892.6, 1433.9), (890.1, 1437.4), (884.8, 1439.5), (886.9, 1444.8), (881.3, 1446.3), (882.2, 1452.0), (880.5, 1455.9), (876.8, 1457.7), (874.5, 1461.2), (870.4, 1462.3), (867.8, 1465.5), (864.8, 1468.6), (859.6, 1466.2), (855.3, 1466.1), (851.2, 1465.6), (847.9, 1470.3), (844.0, 1472.0), (840.1, 1473.4), (836.3, 1469.1), (832.8, 1464.6), (828.6, 1465.1), (823.6, 1468.0), (819.4, 1467.8), (817.4, 1462.4), (813.3, 1461.8), (809.4, 1460.6), (809.0, 1454.9), (801.4, 1457.0), (799.8, 1453.1), (796.3, 1451.0), (795.0, 1447.0), (791.7, 1444.5), (791.1, 1440.3), (785.8, 1438.1), (780.6, 1435.7), (783.6, 1430.8), (784.4, 1426.6), (785.5, 1422.7), (784.3, 1418.8), (786.1, 1415.0), (791.0, 1412.1), (791.1, 1407.9), (798.9, 1407.1), (799.7, 1402.9), (803.0, 1400.5), (804.6, 1396.7), (806.5, 1393.0), (806.8, 1387.3), (809.2, 1383.9), (814.7, 1385.3), (816.4, 1379.8), (821.7, 1382.0), (824.0, 1376.7), (827.4, 1374.4), (831.0, 1372.2), (835.2, 1373.0), (839.3, 1374.1), (843.2, 1372.8), (847.3, 1371.7), (851.0, 1373.6), (855.8, 1370.4), (859.2, 1372.9), (863.5, 1373.1), (867.6, 1373.7), (870.3, 1377.0), (872.7, 1380.5), (876.6, 1382.0), (880.3, 1383.8), (884.0, 1385.9), (887.4, 1388.2), (893.1, 1389.2), (891.5, 1394.7), (896.9, 1396.4), (899.6, 1399.7), (899.3, 1403.8), (898.7, 1407.9), (903.2, 1411.6), (902.0, 1415.6), (906.0, 1419.7), (904.2, 1423.6), (902.1, 1427.2), (899.8, 1430.8), (897.1, 1434.2), (899.1, 1439.5), (895.9, 1442.3), (890.3, 1443.3), (891.1, 1448.9), (885.4, 1448.9), (883.3, 1452.6), (879.3, 1454.0), (876.8, 1457.3), (873.9, 1460.3), (870.8, 1463.1), (865.7, 1460.4), (862.2, 1462.6), (857.7, 1459.1), (853.9, 1460.8), (849.9, 1462.2), (846.0, 1460.5), (841.5, 1463.9), (836.6, 1467.0), (834.1, 1461.8), (831.8, 1456.5), (828.9, 1453.6), (823.4, 1455.2), (820.8, 1451.9), (816.9, 1450.6), (813.0, 1449.0), (813.3, 1443.4), (809.8, 1441.2), (808.7, 1437.1), (805.5, 1434.5), (800.0, 1432.9), (799.6, 1428.7), (796.9, 1425.6), (794.5, 1422.3), (795.0, 1418.1), (798.5, 1413.6), (796.9, 1409.8), (798.4, 1406.1), (797.5, 1402.1), (799.6, 1398.5), (799.3, 1394.3), (801.8, 1391.0), (807.1, 1388.9), (807.5, 1384.8), (810.7, 1382.0), (816.3, 1380.9), (817.7, 1377.0), (821.3, 1374.9), (823.3, 1371.1), (825.5, 1367.5), (826.2, 1361.8), (830.4, 1360.9), (833.3, 1357.8), (837.5, 1357.7), (841.6, 1358.0), (844.6, 1353.2), (848.9, 1357.0), (852.8, 1355.5), (856.9, 1354.5), (860.2, 1359.2), (863.7, 1361.4), (867.9, 1361.2), (870.1, 1366.5), (874.2, 1366.9), (878.4, 1367.6), (882.6, 1368.5), (885.0, 1372.0), (888.9, 1373.6), (888.6, 1379.3), (890.1, 1383.3), (893.4, 1385.7), (894.2, 1389.8), (899.6, 1391.4), (899.7, 1395.6), (902.1, 1399.0), (901.6, 1403.1), (903.5, 1406.9), (902.4, 1411.1), (901.0, 1415.0), (899.4, 1418.8), (897.5, 1422.6), (898.0, 1426.9), (898.2, 1431.1), (898.1, 1435.3), (900.1, 1440.6), (892.1, 1440.6), (891.1, 1444.6), (892.0, 1450.2), (890.4, 1454.0), (886.7, 1455.8), (884.7, 1459.5), (882.4, 1463.1), (879.9, 1466.5), (875.7, 1467.3), (871.6, 1467.7), (867.4, 1467.8), (863.3, 1467.7), (858.6, 1464.5), (854.9, 1466.4), (850.9, 1468.1), (847.0, 1466.7), (842.9, 1467.8), (839.5, 1463.2), (835.3, 1463.7), (831.1, 1464.0), (828.6, 1458.8), (824.4, 1458.4), (820.2, 1457.8), (819.2, 1452.1), (815.3, 1450.8), (809.6, 1450.9), (809.9, 1445.2), (808.7, 1441.4), (807.6, 1437.3), (802.1, 1435.7), (801.7, 1431.6), (799.1, 1428.4), (799.3, 1424.2), (794.5, 1421.1), (795.4, 1417.0), (796.5, 1413.1), (795.3, 1409.1), (794.4, 1404.9), (793.8, 1400.7), (796.1, 1397.1), (796.2, 1392.8), (796.7, 1388.5), (799.8, 1385.6), (798.4, 1380.1), (804.1, 1379.3), (805.7, 1375.4), (809.5, 1373.7), (811.6, 1370.1), (814.2, 1366.6), (818.4, 1365.8), (822.6, 1365.2), (827.8, 1367.4), (832.7, 1370.1), (836.9, 1370.4), (841.0, 1371.0), (844.8, 1369.1), (849.0, 1370.2), (853.0, 1369.0), (857.2, 1368.2), (861.4, 1367.6), (864.9, 1370.0), (869.1, 1370.0), (872.3, 1372.7), (876.4, 1373.2), (881.9, 1371.7), (886.0, 1372.8), (888.1, 1376.3), (888.1, 1382.0), (887.8, 1387.7), (891.3, 1389.9), (894.8, 1392.4), (895.8, 1396.6), (896.5, 1400.6), (899.3, 1403.7), (894.2, 1409.7), (896.6, 1413.0), (901.5, 1416.0), (906.1, 1419.2), (907.7, 1423.1), (909.0, 1427.1), (904.6, 1430.8), (908.1, 1435.2), (905.8, 1438.7), (903.4, 1442.2), (900.7, 1445.3), (902.8, 1450.7), (899.7, 1453.5), (898.9, 1457.7), (897.7, 1461.7), (894.2, 1464.0), (892.6, 1467.8), (890.5, 1471.5), (888.1, 1475.0), (884.0, 1476.1), (882.5, 1481.6), (879.4, 1484.5), (875.2, 1484.6), (871.8, 1487.1), (867.8, 1486.6), (863.7, 1485.9), (859.6, 1484.9), (855.6, 1489.0), (851.5, 1490.0), (848.1, 1485.3), (844.6, 1483.1), (842.0, 1478.0), (837.8, 1477.7), (833.8, 1477.2), (829.7, 1476.5), (825.7, 1475.4), (823.3, 1471.9), (821.2, 1468.3), (819.3, 1464.6), (817.5, 1460.8), (816.0, 1456.9), (810.4, 1455.9), (807.3, 1453.1), (807.0, 1449.0), (804.4, 1445.8), (801.9, 1442.3), (805.2, 1437.6), (806.1, 1433.5), (807.3, 1429.6), (808.8, 1425.8), (807.7, 1421.8), (809.7, 1418.0), (811.9, 1414.4), (814.3, 1411.0), (814.3, 1406.8), (817.2, 1403.6), (817.9, 1399.5), (818.7, 1395.4), (819.9, 1391.4), (823.5, 1389.2), (825.2, 1385.5), (827.3, 1381.8), (828.2, 1376.2), (830.8, 1372.9), (836.2, 1374.6), (839.2, 1371.6), (843.5, 1371.3), (846.8, 1368.7), (851.1, 1369.0), (855.2, 1369.6), (858.6, 1365.1), (862.7, 1366.2), (866.7, 1367.6), (870.1, 1372.2), (873.6, 1374.3), (877.8, 1373.9), (881.1, 1376.4), (886.3, 1373.9), (889.3, 1376.9), (893.5, 1377.6), (897.6, 1378.5), (900.0, 1381.9), (905.6, 1381.3), (907.5, 1385.1), (909.2, 1389.0), (914.9, 1389.5), (916.1, 1393.4), (914.8, 1399.0), (915.4, 1403.2), (913.2, 1408.5), (915.8, 1411.8), (915.5, 1415.9), (920.2, 1419.1), (919.3, 1423.2), (918.0, 1427.2), (919.3, 1431.2), (920.4, 1435.2), (918.6, 1439.0), (916.5, 1442.6), (916.7, 1446.8), (916.6, 1451.0), (913.8, 1454.1), (908.2, 1455.7), (907.3, 1459.8), (908.3, 1465.5), (906.8, 1469.4), (905.1, 1473.3), (899.4, 1472.9), (897.2, 1476.5), (891.5, 1475.4), (888.8, 1478.6), (884.7, 1479.1), (880.5, 1479.3), (876.3, 1479.4), (873.7, 1484.4), (869.4, 1483.8), (865.4, 1483.0), (861.7, 1487.4), (857.7, 1483.3), (853.5, 1484.4), (850.1, 1479.8), (846.0, 1480.5), (841.9, 1480.8), (839.4, 1475.7), (835.1, 1475.4), (830.9, 1474.9), (828.2, 1471.8), (824.2, 1470.8), (820.3, 1469.6), (814.6, 1470.2), (812.6, 1466.5), (808.9, 1464.6), (805.2, 1462.5), (804.0, 1458.4), (803.0, 1454.3), (799.9, 1451.5), (794.5, 1449.7), (794.3, 1445.5), (794.3, 1441.3), (792.0, 1437.9), (792.7, 1433.9), (788.3, 1430.4), (786.8, 1426.5), (791.1, 1422.7), (792.9, 1419.0), (797.6, 1415.8), (797.3, 1411.7), (797.2, 1407.4), (799.9, 1404.2), (800.3, 1399.9), (803.4, 1397.0), (804.1, 1392.9), (807.3, 1390.3), (810.7, 1388.0), (812.3, 1384.1), (814.1, 1380.3), (818.0, 1378.8), (825.3, 1381.9), (826.2, 1376.3), (830.3, 1375.4), (834.5, 1374.7), (837.5, 1371.9), (840.7, 1369.1), (844.2, 1366.6), (847.8, 1364.4), (852.0, 1365.1), (856.4, 1368.7), (860.3, 1367.1), (864.3, 1365.7), (868.1, 1367.3), (871.5, 1371.9), (875.1, 1374.0), (879.2, 1373.6), (883.4, 1373.5), (889.6, 1368.7), (892.7, 1371.6), (896.8, 1372.4), (900.9, 1373.4), (903.3, 1376.8), (905.5, 1380.3), (909.4, 1381.9), (909.3, 1387.6), (915.0, 1387.8), (916.5, 1391.8), (920.0, 1394.2), (921.0, 1398.5), (921.7, 1402.7), (922.1, 1406.8), (924.9, 1410.0), (924.9, 1414.2), (924.6, 1418.5), (926.7, 1422.1), (926.0, 1426.3), (927.7, 1430.1), (929.2, 1433.9), (930.6, 1437.9), (926.3, 1441.6), (924.5, 1445.3), (925.1, 1449.5), (922.9, 1453.0), (923.1, 1457.2), (920.5, 1460.6), (920.2, 1464.7), (919.8, 1468.9), (919.1, 1473.0), (913.5, 1474.2), (914.6, 1479.8), (911.1, 1482.2), (907.6, 1484.4), (903.9, 1486.3), (900.2, 1487.9), (896.3, 1489.5), (894.1, 1493.0), (890.1, 1494.1), (887.6, 1497.4), (884.9, 1500.6), (882.0, 1503.6), (877.9, 1504.0), (872.6, 1501.6), (870.3, 1506.9), (866.1, 1506.7), (862.5, 1509.1), (858.3, 1508.6), (854.2, 1507.9), (850.4, 1509.8), (846.4, 1508.8), (842.5, 1510.4), (838.4, 1509.1), (838.4, 1509.2));
outlined(w5000); outline(w5000, violet); stroke(w5000, 2);
text(wl5000, (830, 1572), "first 5000 primes"); size(wl5000, 20); color(wl5000, dim);
equation(eq, (540, 1660),
`x=\cos(nt+\delta),\ y=\cos(mt);\qquad \delta=j\pi-\tfrac{kn\pi}{m}\ \Rightarrow\ \text{open}`, 22);
text(note, (540, 1770),
"Every prime past 3 is 6k ± 1, so near t = 1/6 the sum has only two step directions — and winds.");
size(note, 20); color(note, dim); wrap(note, 940);
wait(17.0);
orbital-eccentricity
One number, e, and the three separate things it decides. IT DECIDES THE SHAPE: fix the focus and the periapsis and let e run — 0 is a circle, 0<e<1 an ellipse, e=1 a parabola, e>1 a hyperbola. The top panel is that family, every curve sharing one focus and touching at one periapsis; the first three close and the last three leave, and nothing about the curve near periapsis says which side of e = 1 you are on. IT DECIDES THE SPEED: r²·dθ/dt is conserved, so equal areas in equal times — the middle panel is eight sectors of one eighth of a period at e = 0.7, areas 0.280441 to 0.280443 (a 0.0006% spread, quadrature not physics) summing to πab. Short and wide at periapsis, long and narrow at apoapsis. The body is a real Kepler solve, five unrolled Newton steps on M = E − e·sin E, residual 9e-16 at e = 0.7. AND ITS MOST VISIBLE EFFECT IS NOT THE SHAPE: b/a = √(1−e²) ≈ 1 − e²/2 is quadratic in e while c/a = e is linear, so the focus offset is about 2/e times more visible than the flattening — 120 to 1 for Earth, 294 to 1 for Venus. The bottom row is drawn at true eccentricity: Earth’s orbit is a circle to within 0.014%, a seventh of a pixel at the size shown, but the gold Sun sits visibly off the grey centre. “Nearly circular” is true of the shape and quite wrong about where the Sun is.
// orbital-eccentricity — one number, e, and the three separate things it decides.
//
// r(θ) = q(1 + e)/(1 + e·cos θ) q = the periapsis distance
//
// IT DECIDES THE SHAPE. Fix the focus and the periapsis and let e run: e = 0 is a circle,
// 0 < e < 1 an ellipse, e = 1 a parabola, e > 1 a hyperbola. The top panel is that family,
// every curve sharing one focus and touching at one periapsis. The first three close and
// come back. The last three do not — e = 1 is exactly the line between an orbit that returns
// and one that leaves, and nothing about the curve near periapsis tells you which side of it
// you are on.
//
// IT DECIDES THE SPEED. Angular momentum is conserved, so r²·dθ/dt is constant and the body
// sweeps equal areas in equal times. The middle panel is that, at e = 0.7: eight sectors,
// each one eighth of a period. Their areas come out 0.280441 to 0.280443 — a spread of
// 0.0006%, which is the quadrature and not the physics — and they sum to 2.243542 against
// πab = 2.243546. Equal areas mean unequal speeds: v at periapsis over v at apoapsis is
// (1 + e)/(1 − e), which is 1.03 for Earth and 59.6 for Halley's comet.
//
// The body's position is a real Kepler solve, not an eased sweep. M = E − e·sin E has no
// closed form, so the formula runs five unrolled Newton steps from E₀ = M + e·sin M; at
// e = 0.7 that lands the residual at 9e-16, machine precision. Then the position is simply
// (a(cos E − e), b·sin E) about the focus.
//
// AND ITS MOST VISIBLE EFFECT IS NOT THE SHAPE. This is the part that surprises people. The
// shape changes as b/a = √(1 − e²) ≈ 1 − e²/2, but the focus moves out as c/a = e. One is
// quadratic in e and the other linear, so for small e the offset is about 2/e times more
// visible than the flattening:
//
// body e shape off by focus off by ratio
// Venus 0.0068 0.0023% 0.68% 294x
// Earth 0.0167 0.0139% 1.67% 120x
// Mars 0.0934 0.4371% 9.34% 21x
// Mercury 0.2056 2.1364% 20.56% 10x
// Halley 0.967 74.5224% 96.70% 1.3x
//
// The bottom row is drawn at true eccentricity. Earth's orbit is a circle to within 0.014%
// — at the size drawn here the flattening is a seventh of a pixel and could not be shown at
// any scale that fits on a screen. What you can see is the gold dot, the Sun, sitting off
// the grey centre. That offset is the eccentricity. The old claim that the orbits are
// "nearly circular" is true of their SHAPE and quite wrong about where the Sun sits.
//
// manic examples/orbital-eccentricity.manic
title("Orbital eccentricity: one number, three consequences");
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, "Orbital eccentricity: shape, speed, and where the focus sits", (540, 116), 24);
// ---- the family: one focus, one periapsis, e from 0 to 1.4 -------------------------
cloud(family, 6000, #ffffff, 1.0) {
let per = 1000;
let ci = (i - mod(i, per))/per;
let uu = 0 - 1 + 2*(mod(i, per)/999);
let ee = (1 - min(abs(ci - 0), 1))*0.0 + (1 - min(abs(ci - 1), 1))*0.3 + (1 - min(abs(ci - 2), 1))*0.6 + (1 - min(abs(ci - 3), 1))*0.8 + (1 - min(abs(ci - 4), 1))*1.0 + (1 - min(abs(ci - 5), 1))*1.4;
let tm = (1 - min(abs(ci - 0), 1))*3.141593 + (1 - min(abs(ci - 1), 1))*3.141593 + (1 - min(abs(ci - 2), 1))*3.141593 + (1 - min(abs(ci - 3), 1))*2.394730 + (1 - min(abs(ci - 4), 1))*2.142632 + (1 - min(abs(ci - 5), 1))*1.897590;
let th = uu*tm*min(t/14.0, 1);
let rad = 78.0*(1 + ee)/(1 + ee*cos(th));
let x = 680 + rad*cos(th);
let y = 560 - rad*sin(th);
let hue = mod(200 + ci*32, 360);
let sat = 0.78;
let r = 2.1;
}
circle(sun1, (680, 560), 9); color(sun1, gold);
circle(peri, (758, 560), 5); color(peri, dim);
// ---- equal areas in equal times, at e = 0.7 ----------------------------------------
cloud(sectors, 12000, #ffffff, 1.0) {
let per = 1500;
let sj = (i - mod(i, per))/per; // which of the eight sectors
let kk = mod(i, per);
// an R2 low-discrepancy fill, with sqrt on the radial coordinate so the density is
// uniform per unit AREA rather than per unit radius — a grid here moirés badly
let su = sqrt(mod(kk*0.7548776662, 1));
let mv = mod(kk*0.5698402910, 1);
let mm = 0.7853982*(sj + mv); // mean anomaly, one eighth per sector
let ea = mm + 0.7*sin(mm);
let e1 = ea - (ea - 0.7*sin(ea) - mm)/(1 - 0.7*cos(ea));
let e2 = e1 - (e1 - 0.7*sin(e1) - mm)/(1 - 0.7*cos(e1));
let e3 = e2 - (e2 - 0.7*sin(e2) - mm)/(1 - 0.7*cos(e2));
let e4 = e3 - (e3 - 0.7*sin(e3) - mm)/(1 - 0.7*cos(e3));
let e5 = e4 - (e4 - 0.7*sin(e4) - mm)/(1 - 0.7*cos(e4));
let x = 673 + su*190*(cos(e5) - 0.7);
let y = 1130 - su*135.7*sin(e5);
let hue = 32 + 150*mod(sj, 2);
let sat = 0.62;
let r = 2.6;
let alpha = 0.28*min(max(8*min(t/14.0, 1) - sj, 0), 1);
}
// the eight dividing radii, so the wedges are bounded and countable
cloud(spokes, 1080, #ffffff, 1.0) {
let per = 135;
let sj = (i - mod(i, per))/per;
let su = mod(i, per)/134;
let mm = 0.7853982*sj;
let ea = mm + 0.7*sin(mm);
let e1 = ea - (ea - 0.7*sin(ea) - mm)/(1 - 0.7*cos(ea));
let e2 = e1 - (e1 - 0.7*sin(e1) - mm)/(1 - 0.7*cos(e1));
let e3 = e2 - (e2 - 0.7*sin(e2) - mm)/(1 - 0.7*cos(e2));
let e4 = e3 - (e3 - 0.7*sin(e3) - mm)/(1 - 0.7*cos(e3));
let e5 = e4 - (e4 - 0.7*sin(e4) - mm)/(1 - 0.7*cos(e4));
let x = 673 + su*190*(cos(e5) - 0.7);
let y = 1130 - su*135.7*sin(e5);
let sat = 0;
let r = 1.4;
let alpha = 0.55*min(max(8*min(t/14.0, 1) - sj, 0), 1);
}
cloud(orbit7, 1400, #ffffff, 1.0) {
let ph = i/1400*6.283185;
let x = 673 + 190*(cos(ph) - 0.7);
let y = 1130 - 135.7*sin(ph);
let sat = 0;
let r = 1.8;
let alpha = 0.75;
}
cloud(body, 100, #ffffff, 1.0) {
let mm = 6.283185*min(t/14.0, 1);
let ea = mm + 0.7*sin(mm);
let e1 = ea - (ea - 0.7*sin(ea) - mm)/(1 - 0.7*cos(ea));
let e2 = e1 - (e1 - 0.7*sin(e1) - mm)/(1 - 0.7*cos(e1));
let e3 = e2 - (e2 - 0.7*sin(e2) - mm)/(1 - 0.7*cos(e2));
let e4 = e3 - (e3 - 0.7*sin(e3) - mm)/(1 - 0.7*cos(e3));
let e5 = e4 - (e4 - 0.7*sin(e4) - mm)/(1 - 0.7*cos(e4));
let a = i/100*6.283185;
let rr = 8*(i/100);
let x = 673 + 190*(cos(e5) - 0.7) + rr*cos(a*9);
let y = 1130 - 135.7*sin(e5) + rr*sin(a*9);
let hue = 190;
let sat = 0.6;
let r = 2.4;
}
circle(sun2, (673, 1130), 8); color(sun2, gold);
// ---- real orbits, at true eccentricity ---------------------------------------------
cloud(o_earth, 900, #ffffff, 1.0) {
let ph = i/900*6.283185;
let x = 190 + 105.0*cos(ph);
let y = 1500 - 104.9854*sin(ph);
let hue = 30;
let sat = 0.75;
let r = 2.0;
}
circle(f_earth, (191.75, 1500), 6); color(f_earth, gold);
circle(c_earth, (190, 1500), 3); color(c_earth, dim);
text(n_earth, (190, 1636), "Earth"); size(n_earth, 21); color(n_earth, fg);
text(e_earth, (190, 1668), "e = 0.0167"); size(e_earth, 18); color(e_earth, dim);
cloud(o_mars, 900, #ffffff, 1.0) {
let ph = i/900*6.283185;
let x = 425 + 105.0*cos(ph);
let y = 1500 - 104.5410*sin(ph);
let hue = 85;
let sat = 0.75;
let r = 2.0;
}
circle(f_mars, (434.81, 1500), 6); color(f_mars, gold);
circle(c_mars, (425, 1500), 3); color(c_mars, dim);
text(n_mars, (425, 1636), "Mars"); size(n_mars, 21); color(n_mars, fg);
text(e_mars, (425, 1668), "e = 0.0934"); size(e_mars, 18); color(e_mars, dim);
cloud(o_mercury, 900, #ffffff, 1.0) {
let ph = i/900*6.283185;
let x = 660 + 105.0*cos(ph);
let y = 1500 - 102.7568*sin(ph);
let hue = 140;
let sat = 0.75;
let r = 2.0;
}
circle(f_mercury, (681.59, 1500), 6); color(f_mercury, gold);
circle(c_mercury, (660, 1500), 3); color(c_mercury, dim);
text(n_mercury, (660, 1636), "Mercury"); size(n_mercury, 21); color(n_mercury, fg);
text(e_mercury, (660, 1668), "e = 0.2056"); size(e_mercury, 18); color(e_mercury, dim);
cloud(o_halley, 900, #ffffff, 1.0) {
let ph = i/900*6.283185;
let x = 895 + 105.0*cos(ph);
let y = 1500 - 26.7515*sin(ph);
let hue = 195;
let sat = 0.75;
let r = 2.0;
}
circle(f_halley, (996.53, 1500), 6); color(f_halley, gold);
circle(c_halley, (895, 1500), 3); color(c_halley, dim);
text(n_halley, (895, 1636), "Halley"); size(n_halley, 21); color(n_halley, fg);
text(e_halley, (895, 1668), "e = 0.967"); size(e_halley, 18); color(e_halley, dim);
equation(eq, (540, 1760),
`\frac{b}{a}=\sqrt{1-e^2}\approx 1-\tfrac{e^2}{2},\qquad \frac{c}{a}=e`, 25);
text(note, (540, 1860),
"The shape is quadratic in e; the focus offset is linear. For Earth that is 120 to 1.");
size(note, 20); color(note, dim); wrap(note, 900);
wait(17.0);
caustics
A caustic is where reflected rays crowd — the envelope of what a mirror sends back. caustic-family follows one mirror as the lamp slides; this follows four mirrors, and the point is that the answer is almost never a new curve. TWO COLLAPSE TO A POINT: a parabola under rays parallel to its axis sends every reflected ray through the focus (distance from it, 1e-15 — machine precision, not a fit), which is why a dish is a dish; and an ellipse lit from one focus sends every ray through the OTHER focus to 5e-10, which is the whole of a whispering gallery. TWO GIVE BACK A CURVE YOU KNOW: a cycloid arch under perpendicular rays has for its caustic TWO CYCLOID ARCHES at exactly half the size (checked to 6e-04, the sampling) — and its reflected direction comes out (−sin u, −cos u), already a unit vector, which is a hint the answer was going to be tidy; and y = eˣ under vertical rays gives a CATENARY, not merely catenary-shaped — fitting A·cosh((x−x₀)/A) + c returns A = 1.0000, x₀ = −1.0000, c = 0.0000 with residual 3e-08, so it is y = cosh(x+1) exactly. One more, checked but not drawn: a logarithmic spiral lit from the point it winds onto returns a logarithmic spiral of the SAME pitch (k = 0.300000 for a source k of 0.3), and since scaling a log spiral is rotating it, that curve is its own caustic. Nothing draws a caustic here: every ray is d′ = d − 2(d·n)n against the real normal, and the cyan point or curve is placed independently for the envelope to land on.
// 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);
fresnel-wave
Fresnel’s wave surface, 1821: the wavefront of light inside a biaxial crystal, and a quartic you can build with a ruler and a story. Take an ellipsoid a > b > c; for every plane through the centre, measure the two semi-axes of the ellipse it cuts and plot BOTH along that plane’s normal. Two lengths per direction, so two sheets, one inside the other. It is closed-form and therefore drawable from a formula: the 2×2 restriction of Q = diag(1/a²,1/b²,1/c²) to n⊥ has trace T = tr Q − nᵀQn and determinant D = det Q · nᵀQ⁻¹n, so λ = (T ± √(T²−4D))/2 and r = 1/√λ — checked against a real eigendecomposition over 4000 directions, 3e-14, no iteration anywhere. THE FOUR SINGULAR POINTS: the sheets touch where T² = 4D, which is where the central section is a CIRCLE rather than an ellipse. An ellipsoid has exactly two circular sections, normals in the x–z plane at tanθ = (c/a)√((a²−b²)/(b²−c²)) — ±31.05° here, both radii exactly b — so ±each gives FOUR points, and nowhere off that plane does the discriminant vanish. Those are the crystal’s OPTIC AXES: light sent along one does not split into two rays, it spreads into a hollow cone, which Hamilton predicted from this surface in 1832 and Lloyd found two months later. Each principal plane cuts the surface in a circle and an ellipse whose axes come out SWAPPED relative to the ellipsoid’s (to 1e-15), and only in y = 0 do they cross — that plane holds the optic axes, and the crossings are the singular points seen edge-on.
// 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);
zoo-exponents
mathcurve’s herbarium filed the way its own table files it — by the EXPONENTS in y² = xᵃ − xᵇ. Nine named curves, each on its own axes, and the point is that they are not nine ideas but one equation with the exponents turned. THE THREE CUBICS ARE THE SAME CUBIC: y² = x³, y² = x²(x−1) and y² = x²(1−x) differ by one shift and one sign, and each gets a different KIND of singularity at the origin — the complete trichotomy for a cubic, in three drawings. y² = x³ is real only for x ≥ 0 with y ≈ ±x^(3/2), so both branches leave with the same tangent: a CUSP. y² = x²(x−1) is negative on 0 < x < 1, so no real curve comes near the origin — yet (0,0) satisfies it: an ACNODE, a lone real point, with the curve itself starting only at x = 1, which the axes let you read off. y² = x²(1−x) is positive either side with y ≈ ±x: two branches, two tangents, a CRUNODE whose loop closes at x = 1. THE LAST THREE ARE ONE CURVE WITH ONE KNOB: y² = xᵃ − x² is real exactly where |x| ≤ 1, so all three live in the unit strip and a decides only how they fill it — a = 2/3 and 4/3 are even in x and get two lobes (dipole, double egg), a = 3/2 needs √x and gets one (simple folium). The names describe the parity of an exponent. Drawn with plot and param — the maths kit drawing maths — and three needed closed parametrisations verified against their implicit equations first, to 1e-16.
// zoo-exponents — mathcurve's herbarium, filed the way its own table files it: by the
// EXPONENTS in y² = xᵃ − xᵇ. Nine named curves in one frame, and the point of putting them
// together is that they are not nine ideas. They are one equation with the exponents turned.
//
// y = x² parabola y³ = 1 − x³ Lamé cubic
// y² = x² + 1 hyperbola y² = x^(2/3) − x² dipole curve
// y² = x³ semicubical parabola y² = x^(4/3) − x² double egg
// y² = x²(x − 1) duplicating cubic y² = x^(3/2) − x² simple folium
// y² = x²(1 − x) divergent parabola
//
// THE THREE CUBICS ARE THE SAME CUBIC. y² = x³, y² = x²(x − 1) and y² = x²(1 − x) differ by
// one shift and one sign, and each gets a different KIND of singularity at the origin — which
// is the complete trichotomy for a cubic, here as three drawings of nearly the same formula:
//
// y² = x³ real only for x ≥ 0, and y ≈ ±x^(3/2) near 0, so both branches leave with
// the SAME tangent. A CUSP.
// y² = x²(x−1) negative on 0 < x < 1, so no real curve comes near the origin at all —
// and yet (0,0) satisfies the equation. An ACNODE: a lone real point, drawn
// in cyan, with the curve itself starting only at x = 1.
// y² = x²(1−x) positive either side of 0, and y ≈ ±x: two branches, two tangents. A
// CRUNODE, and the loop it opens closes again at x = 1.
//
// One sign decides which. Each is marked on its own panel, so the trichotomy is something to
// look at rather than something to be told.
//
// THE LAST THREE ARE ONE CURVE WITH ONE KNOB. y² = xᵃ − x² is real exactly where |x|ᵃ ≥ x²,
// which for a < 2 means |x| ≤ 1 — so all three live in the unit strip and a decides only how
// they fill it. a = 2/3 and a = 4/3 are EVEN in x, so both get two lobes: the dipole curve
// and the double egg. a = 3/2 needs a square root of x, exists only for x ≥ 0, and gets one:
// the simple folium. The names are describing the parity of an exponent.
//
// The double egg is usually met as r = cos²θ. That the polar form and the exponent form are
// the same curve is checked rather than assumed — substituting one into the other leaves
// 2e-16.
//
// Every cell carries its own axes, because every claim on this page is about where the origin
// is or how far out x reaches: the three singularities all sit AT (0,0), and the last three
// curves are the ones that stop at |x| = 1. Without a frame you would have to take that on
// trust; with one you can read it off.
//
// Every curve here is a `plot` or a `param` — the maths kit drawing maths, with real strokes
// that `draw` reveals. Three of them needed a closed parametrisation that did not exist ready
// made, and each was verified against its own implicit equation before being drawn:
// the dipole as (cos³t, cos t·sin t·√(1+cos²t)) to 3e-16, the double egg as (cos³t, cos²t·sin t)
// to 2e-16, and the simple folium over x = (1−cos t)/2 to 1e-16.
//
// manic examples/zoo-exponents.manic
title("Nine famous curves, one equation in disguise");
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, "Nine names. One equation. Only the exponents differ.", (540, 116), 25);
coords(k_pb, (400.00, 460.50), (-1.4000, 1.4000), (-0.4200, 2.3800), 82.1429, 82.1429, 1, 1.0, 1);
color(k_pb, dim); opacity(k_pb, 0.45);
coords(k_hy, (680.00, 380.00), (-1.6008, 1.6008), (-1.6008, 1.6008), 71.8399, 71.8399, 1, 1.0, 1);
color(k_hy, dim); opacity(k_hy, 0.45);
coords(k_sc, (220.00, 770.00), (-0.9468, 2.3392), (-1.6430, 1.6430), 71.8184, 71.8184, 1, 1.0, 1);
color(k_sc, dim); opacity(k_sc, 0.45);
coords(k_du, (462.09, 770.00), (-0.8260, 4.0360), (-2.4310, 2.4310), 48.5397, 48.5397, 1, 1.0, 1);
color(k_du, dim); opacity(k_du, 0.45);
coords(k_dv, (820.92, 770.00), (-2.3751, 1.9726), (-2.1739, 2.1739), 54.2809, 54.2809, 1, 1.0, 1);
color(k_dv, dim); opacity(k_dv, 0.45);
coords(k_lm, (175.00, 1162.85), (-2.3000, 2.3000), (-2.2369, 2.3631), 45.2174, 45.2174, 1, 1.0, 1);
color(k_lm, dim); opacity(k_lm, 0.45);
coords(k_dp, (420.00, 1160.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 104.0000, 104.0000, 1, 0.5, 1);
color(k_dp, dim); opacity(k_dp, 0.45);
coords(k_de, (665.00, 1160.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 104.0000, 104.0000, 1, 0.5, 1);
color(k_de, dim); opacity(k_de, 0.45);
coords(k_sf, (806.00, 1160.00), (0.0000, 1.0000), (-0.5000, 0.5000), 208.0000, 208.0000, 1, 0.25, 1);
color(k_sf, dim); opacity(k_sf, 0.45);
plot(pb, (400.00, 460.50), 82.1429, 82.1429, "x*x", (-1.4, 1.4));
color(pb, gold); stroke(pb, 4); untraced(pb);
plot(hy0, (680.00, 380.00), 71.8399, 71.8399, "sqrt(x*x + 1)", (-1.25, 1.25));
color(hy0, gold); stroke(hy0, 4); untraced(hy0);
plot(hy1, (680.00, 380.00), 71.8399, 71.8399, "0 - sqrt(x*x + 1)", (-1.25, 1.25));
color(hy1, gold); stroke(hy1, 4); untraced(hy1);
param(sc, (220.00, 770.00), 71.8184, 71.8184,
"t*t", "t*t*t", (-1.18, 1.18));
color(sc, mint); stroke(sc, 4); untraced(sc);
param(du, (462.09, 770.00), 48.5397, 48.5397,
"1 + t*t", "(1 + t*t)*t", (-1.1, 1.1));
color(du, mint); stroke(du, 4); untraced(du);
param(dv, (820.92, 770.00), 54.2809, 54.2809,
"1 - t*t", "(1 - t*t)*t", (-1.55, 1.55));
color(dv, mint); stroke(dv, 4); untraced(dv);
plot(lm, (175.00, 1162.85), 45.2174, 45.2174, "sign(1 - x*x*x)*exp(ln(abs(1 - x*x*x) + 0.000001)/3)", (-2.3, 2.3));
color(lm, violet); stroke(lm, 4); untraced(lm);
param(dp, (420.00, 1160.00), 104.0000, 104.0000,
"cos(t)*cos(t)*cos(t)", "cos(t)*sin(t)*sqrt(1 + cos(t)*cos(t))", (0, 6.283185));
color(dp, violet); stroke(dp, 4); untraced(dp);
param(de, (665.00, 1160.00), 104.0000, 104.0000,
"cos(t)*cos(t)*cos(t)", "cos(t)*cos(t)*sin(t)", (0, 6.283185));
color(de, violet); stroke(de, 4); untraced(de);
param(sf, (806.00, 1160.00), 208.0000, 208.0000,
"(1 - cos(t))/2", "sign(sin(t) + 0.000001)*sqrt(max(exp(1.5*ln((1 - cos(t))/2 + 0.000000001)) - (1 - cos(t))/2*(1 - cos(t))/2, 0))", (0, 6.283185));
color(sf, violet); stroke(sf, 4); untraced(sf);
text(n_pb, (400, 531), "parabola"); size(n_pb, 20); color(n_pb, fg);
equation(q_pb, (400, 569), `y=x^2`, 26);
text(n_hy, (680, 531), "hyperbola"); size(n_hy, 20); color(n_hy, fg);
equation(q_hy, (680, 569), `y^2=x^2+1`, 26);
text(n_sc, (270, 924), "semicubical parabola"); size(n_sc, 20); color(n_sc, fg);
equation(q_sc, (270, 962), `y^2=x^3`, 26);
circle(s_sc, (220.00, 770.00), 7); color(s_sc, gold);
text(m_sc, (270, 988), "cusp at the origin"); size(m_sc, 17); color(m_sc, gold);
text(n_du, (540, 924), "duplicating cubic"); size(n_du, 20); color(n_du, fg);
equation(q_du, (540, 962), `y^2=x^2(x-1)`, 26);
circle(s_du, (462.09, 770.00), 7); color(s_du, cyan);
text(m_du, (540, 988), "acnode at the origin"); size(m_du, 17); color(m_du, cyan);
text(n_dv, (810, 924), "divergent parabola"); size(n_dv, 20); color(n_dv, fg);
equation(q_dv, (810, 962), `y^2=x^2(1-x)`, 26);
circle(s_dv, (820.92, 770.00), 7); color(s_dv, magenta);
text(m_dv, (810, 988), "crunode at the origin"); size(m_dv, 17); color(m_dv, magenta);
text(n_lm, (175, 1300), "Lamé cubic"); size(n_lm, 20); color(n_lm, fg);
equation(q_lm, (175, 1338), `y^3=1-x^3`, 26);
text(n_dp, (420, 1300), "dipole curve"); size(n_dp, 20); color(n_dp, fg);
equation(q_dp, (420, 1338), `y^2=x^{2/3}-x^2`, 26);
text(n_de, (665, 1300), "double egg"); size(n_de, 20); color(n_de, fg);
equation(q_de, (665, 1338), `y^2=x^{4/3}-x^2`, 26);
text(n_sf, (910, 1300), "simple folium"); size(n_sf, 20); color(n_sf, fg);
equation(q_sf, (910, 1338), `y^2=x^{3/2}-x^2`, 26);
text(n1, (540, 1400),
"The three cubics differ by one sign — and the origin is a cusp, then an isolated point, then a loop.");
size(n1, 20); color(n1, fg); wrap(n1, 960);
text(n2, (540, 1500),
"y² = xᵃ − x² is real exactly where |x| ≤ 1. Only the parity of a decides one lobe or two.");
size(n2, 20); color(n2, dim); wrap(n2, 960);
equation(eq, (540, 1640), `y^2=x^{a}-x^{b}`, 46);
// the conics first, then the three cubics one at a time so the trichotomy reads as a
// sequence, then the exponent family
par {
draw(pb, 3.0);
draw(hy0, 3.0);
draw(hy1, 3.0);
seq { wait(2.6); draw(sc, 3.0); }
seq { wait(4.2); draw(du, 3.0); }
seq { wait(5.8); draw(dv, 3.0); }
seq { wait(7.6); draw(lm, 3.2); }
seq { wait(8.7); draw(dp, 3.2); }
seq { wait(9.8); draw(de, 3.2); }
seq { wait(10.9); draw(sf, 3.2); }
}
wait(3.5);
zoo-exponents-2
The second half of mathcurve’s exponent table, same trick as zoo-exponents: seven named curves that are one equation with the powers moved. THE EIGHT CURVE AND THE CAMPYLE ARE THE SAME TWO FACTORS WITH THE SIGN FLIPPED — x²(1−x²) against x²(x²−1). One is real exactly where the other is not: swept at 200,000 points across x ∈ [−3,3] there is NO x where both are real and none where neither is. They partition the line, |x| < 1 to the eight curve and |x| > 1 to the campyle, touching only at 0 and ±1. One is a bounded figure of eight, the other runs to infinity in four directions, and a minus sign is the whole difference. ONE EXPONENT SEPARATES A CROSSING FROM A PINCH: y² = x²(1−x²) gives y/x → 1 at the origin, two tangents, a NODE; y² = x⁴(1−x²) gives y/x → 0, both branches arriving flat, a TACNODE — the double teardrop’s waist is a crossing ironed out. THE MOUTH AND THE ASTROID ARE THE SAME CUBE: (1−x²)³ and (1−x^(2/3))³ differ only inside the bracket, and that decides how many corners it gets — counting where the velocity vanishes, the mouth has TWO cusps and the astroid FOUR. The bifolium is given as ±√x ± √(x(1−x)), four sign choices and four arcs, and all four fall out of one parametrisation x = sin²θ, y = sin θ(1 + cos θ) that walks the signs as θ crosses π/2, π and 3π/2.
// zoo-exponents-2 — the second half of mathcurve's exponent table, and the same trick as
// examples/zoo-exponents.manic: seven named curves that are one equation with the powers
// moved.
//
// y² = x²(1 − x²) eight curve y² = (1 − x²)³ mouth
// y² = x³(1 − x) piriform quartic y² = (1 − x^(2/3))³ astroid
// y² = x⁴(1 − x²) double teardrop y = ±√x ± √(x(1−x)) bifolium
// y² = x²(x² − 1) campyle of Eudoxus
//
// THE EIGHT CURVE AND THE CAMPYLE ARE THE SAME TWO FACTORS WITH THE SIGN FLIPPED.
// x²(1 − x²) against x²(x² − 1). One is real exactly where the other is not: swept at two
// hundred thousand points across x ∈ [−3, 3], there is NO x where both are real and none
// where neither is. They partition the line — |x| < 1 belongs to the eight curve, |x| > 1 to
// the campyle — and they touch only at x = 0 and ±1, where both vanish. One is a bounded
// figure of eight; the other runs off to infinity in four directions; and the only difference
// between them is a minus sign.
//
// ONE EXPONENT SEPARATES A CROSSING FROM A PINCH. y² = x²(1 − x²) and y² = x⁴(1 − x²) differ
// in one power. Near the origin the first gives y/x → 1: two branches with two distinct
// tangents, a NODE. The second gives y/x → 0.0001 at x = 10⁻⁴, i.e. → 0: both branches arrive
// flat and touch instead of crossing, a TACNODE. Same picture otherwise, and the waist of the
// double teardrop is a crossing that has been ironed out.
//
// THE MOUTH AND THE ASTROID ARE THE SAME CUBE. y² = (1 − x²)³ and y² = (1 − x^(2/3))³ differ
// only in the exponent inside the bracket, and that decides how many corners the cube gets:
// counting points where the parametrisation's velocity vanishes, the mouth has TWO cusps, at
// (±1, 0), and the astroid has FOUR, at (±1, 0) and (0, ±1). Cubing a bracket makes cusps;
// what is inside the bracket says how many.
//
// The bifolium is given as a sum of two square roots rather than a polynomial, and its four
// sign choices are four arcs. They close into two lobes, and all four fall out of a single
// parametrisation — x = sin²θ, y = sin θ(1 + cos θ) — which walks the signs (+,+), (+,−),
// (−,+), (−,−) as θ crosses π/2, π and 3π/2. Checked against the stated form: 3e-14.
//
// Every curve is a `param` and every cell has its own axes, because the claims are positional
// — where a curve is real, where it is singular, and where it stops. Each parametrisation was
// verified against its own implicit equation before being drawn; the worst of the seven is
// 9e-15 and the rest are at machine precision.
//
// manic examples/zoo-exponents-2.manic
title("Seven more curves, and the powers that separate them");
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, "One minus sign apart. One exponent apart.", (540, 116), 25);
coords(k_hui, (175.00, 440.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 118.0000, 118.0000, 1, 0.5, 1);
color(k_hui, dim); opacity(k_hui, 0.45);
coords(k_pir, (302.00, 440.00), (0.0000, 1.0000), (-0.5000, 0.5000), 236.0000, 236.0000, 1, 0.25, 1);
color(k_pir, dim); opacity(k_pir, 0.45);
coords(k_dbt, (665.00, 440.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 118.0000, 118.0000, 1, 0.5, 1);
color(k_dbt, dim); opacity(k_dbt, 0.45);
coords(k_cam, (910.00, 440.00), (-3.4855, 3.4855), (-3.4855, 3.4855), 33.8549, 33.8549, 1, 2.0, 1);
color(k_cam, dim); opacity(k_cam, 0.45);
coords(k_mou, (275.00, 980.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 130.0000, 130.0000, 1, 0.5, 1);
color(k_mou, dim); opacity(k_mou, 0.45);
coords(k_ast, (540.00, 980.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 130.0000, 130.0000, 1, 0.5, 1);
color(k_ast, dim); opacity(k_ast, 0.45);
coords(k_bif, (754.96, 980.00), (-0.7990, 1.7990), (-1.2990, 1.2990), 100.0742, 100.0742, 1, 0.5, 1);
color(k_bif, dim); opacity(k_bif, 0.45);
param(hui, (175.00, 440.00), 118.0000, 118.0000,
"sin(t)", "sin(t)*cos(t)", (0, 6.283185));
color(hui, mint); stroke(hui, 4); untraced(hui);
param(pir, (302.00, 440.00), 236.0000, 236.0000,
"(1 + sin(t))/2", "cos(t)*(1 + sin(t))/4", (0, 6.283185));
color(pir, mint); stroke(pir, 4); untraced(pir);
param(dbt, (665.00, 440.00), 118.0000, 118.0000,
"sin(t)", "sin(t)*sin(t)*cos(t)", (0, 6.283185));
color(dbt, mint); stroke(dbt, 4); untraced(dbt);
param(cam0, (910.00, 440.00), 33.8549, 33.8549,
"cosh(t)", "cosh(t)*sinh(t)", (-1.32, 1.32));
color(cam0, violet); stroke(cam0, 4); untraced(cam0);
param(cam1, (910.00, 440.00), 33.8549, 33.8549,
"0 - cosh(t)", "cosh(t)*sinh(t)", (-1.32, 1.32));
color(cam1, violet); stroke(cam1, 4); untraced(cam1);
param(mou, (275.00, 980.00), 130.0000, 130.0000,
"sin(t)", "cos(t)*cos(t)*cos(t)", (0, 6.283185));
color(mou, gold); stroke(mou, 4); untraced(mou);
param(ast, (540.00, 980.00), 130.0000, 130.0000,
"cos(t)*cos(t)*cos(t)", "sin(t)*sin(t)*sin(t)", (0, 6.283185));
color(ast, gold); stroke(ast, 4); untraced(ast);
param(bif, (754.96, 980.00), 100.0742, 100.0742,
"sin(t)*sin(t)", "sin(t)*(1 + cos(t))", (0, 6.283185));
color(bif, cyan); stroke(bif, 4); untraced(bif);
text(n_hui, (175, 594), "eight curve"); size(n_hui, 20); color(n_hui, fg);
equation(q_hui, (175, 632), `y^2=x^2(1-x^2)`, 26);
circle(s0_hui, (175.0, 440.0), 6); color(s0_hui, magenta);
text(m_hui, (175, 666), "node"); size(m_hui, 17); color(m_hui, magenta);
text(n_pir, (420, 594), "piriform quartic"); size(n_pir, 20); color(n_pir, fg);
equation(q_pir, (420, 632), `y^2=x^3(1-x)`, 26);
circle(s0_pir, (302.0, 440.0), 6); color(s0_pir, gold);
text(m_pir, (420, 666), "cusp"); size(m_pir, 17); color(m_pir, gold);
text(n_dbt, (665, 594), "double teardrop"); size(n_dbt, 20); color(n_dbt, fg);
equation(q_dbt, (665, 632), `y^2=x^4(1-x^2)`, 26);
circle(s0_dbt, (665.0, 440.0), 6); color(s0_dbt, cyan);
text(m_dbt, (665, 666), "tacnode"); size(m_dbt, 17); color(m_dbt, cyan);
text(n_cam, (910, 594), "campyle of Eudoxus"); size(n_cam, 20); color(n_cam, fg);
equation(q_cam, (910, 632), `y^2=x^2(x^2-1)`, 26);
text(n_mou, (275, 1146), "mouth"); size(n_mou, 20); color(n_mou, fg);
equation(q_mou, (275, 1184), `y^2=(1-x^2)^3`, 26);
circle(s0_mou, (405.0, 980.0), 6); color(s0_mou, coral);
circle(s1_mou, (145.0, 980.0), 6); color(s1_mou, coral);
text(m_mou, (275, 1218), "2 cusps"); size(m_mou, 17); color(m_mou, coral);
text(n_ast, (540, 1146), "astroid"); size(n_ast, 20); color(n_ast, fg);
equation(q_ast, (540, 1184), `y^2=(1-x^{2/3})^3`, 26);
circle(s0_ast, (670.0, 980.0), 6); color(s0_ast, coral);
circle(s1_ast, (410.0, 980.0), 6); color(s1_ast, coral);
circle(s2_ast, (540.0, 850.0), 6); color(s2_ast, coral);
circle(s3_ast, (540.0, 1110.0), 6); color(s3_ast, coral);
text(m_ast, (540, 1218), "4 cusps"); size(m_ast, 17); color(m_ast, coral);
text(n_bif, (805, 1146), "bifolium"); size(n_bif, 20); color(n_bif, fg);
equation(q_bif, (805, 1184), `y=\pm\sqrt{x}\pm\sqrt{x(1-x)}`, 26);
circle(s0_bif, (755.0, 980.0), 6); color(s0_bif, magenta);
text(m_bif, (805, 1218), "both lobes meet"); size(m_bif, 17); color(m_bif, magenta);
text(n1, (540, 1310),
"x²(1 − x²) and x²(x² − 1): one is real exactly where the other is not. They share only x = 0, ±1.");
size(n1, 20); color(n1, fg); wrap(n1, 980);
text(n2, (540, 1410),
"Cubing a bracket makes cusps. What is inside it decides how many: two for the mouth, four for the astroid.");
size(n2, 20); color(n2, dim); wrap(n2, 980);
equation(eq, (540, 1560), `y^2=x^{m}(1-x^{n})`, 46);
// the four x^m(1 - x^n) curves first, then the sign flip, then the cubed bracket, then the
// bifolium — so each comparison lands next to the one it argues with
par {
draw(hui, 3.0);
seq { wait(1.6); draw(pir, 3.0); }
seq { wait(3.2); draw(dbt, 3.0); }
seq { wait(5.0); draw(cam0, 3.0); }
seq { wait(5.0); draw(cam1, 3.0); }
seq { wait(7.0); draw(mou, 3.2); }
seq { wait(8.8); draw(ast, 3.2); }
seq { wait(10.6); draw(bif, 3.4); }
}
wait(3.6);
zoo-exponents-3
The third of mathcurve’s exponent tables, and the one where a DENOMINATOR arrives. Eight named curves filed by the only thing a denominator does — none (1 + x² never vanishes: the witch of Agnesi, the serpentine), one (the trident of Newton at x = 0, the mixed cubic and the cissoid of Diocles at x = 1), or two (double u, puntiforme, cruciform at x = ±1). The asymptotes are drawn in coral, because that is the classification. PUNTIFORME AND CRUCIFORM PARTITION THE LINE: x²/(1−x²) against x²/(x²−1), the same fraction upside down — swept at 200,000 points there is no x where both are real and none where neither is, |x| < 1 to one and |x| > 1 to the other. Same trick the eight curve and campyle play in zoo-exponents-2, done with a division instead of a product. THE NUMERATOR DECIDES THE ORIGIN: all three two-asymptote curves share poles at x = ±1, and only the top differs. 1/(1−x²) has no root, so |y| ≥ 1 and the curve is two U-shapes that never approach the axis — which is the name. x²/(1−x²) sends it through (0,0) with slope ±1. x²/(x²−1) keeps the x² but flips the sign below, so (0,0) still satisfies the equation while no real curve comes near it: an ACNODE. The cruciform is the only one with horizontal asymptotes too, y → ±1 (at x = 1000, y = 1.000000500). The parametrisations are the tidy ones — the cissoid as (sin²t, sin³t/cos t), the cruciform as simply (sec t, csc t), one branch per quadrant — each verified against its implicit equation first.
// zoo-exponents-3 — the third of mathcurve's exponent tables, and the one where a
// DENOMINATOR arrives. Eight named curves, filed by the only thing the denominator does:
//
// NO VERTICAL ASYMPTOTE 1 + x² never vanishes
// y = 1/(1 + x²) witch of Agnesi
// y = x/(1 + x²) serpentine
//
// ONE one real root under the line
// y² = (x³ + 1)/x trident of Newton x = 0
// y² = x²/(x − 1) mixed cubic x = 1
// y² = x³/(1 − x) cissoid of Diocles x = 1
//
// TWO 1 − x² and x² − 1
// y² = 1/(1 − x²) double u x = ±1
// y² = x²/(1 − x²) puntiforme x = ±1
// y² = x²/(x² − 1) cruciform x = ±1, and y = ±1
//
// The asymptotes are drawn, in coral. That is the whole classification: the denominator says
// where the curve leaves, the numerator says what it does at the origin.
//
// PUNTIFORME AND CRUCIFORM PARTITION THE LINE. x²/(1 − x²) against x²/(x² − 1) — the same
// fraction upside down. Swept at two hundred thousand points, there is no x where both are
// real and none where neither is: |x| < 1 belongs to one and |x| > 1 to the other. This is
// the same trick the eight curve and the campyle play in
// examples/zoo-exponents-2.manic, done with a division instead of a product.
//
// THE NUMERATOR DECIDES THE ORIGIN. All three of the two-asymptote curves have the same poles
// at x = ±1. y² = 1/(1 − x²) has no root at all, so |y| ≥ 1 and the curve is two U-shapes
// that never come near the axis — which is the name. y² = x²/(1 − x²) puts an x² on top, so
// the curve passes through (0, 0) with slope ±1. And y² = x²/(x² − 1) keeps that x² but
// flips the sign below, so (0,0) still satisfies the equation while no real curve comes near
// it: an ACNODE again, marked in cyan, with the four branches out past |x| = 1.
//
// The cruciform is the only one here with HORIZONTAL asymptotes too: y² = x²/(x² − 1) → 1 as
// x → ∞, so y → ±1. Checked: at x = 1000, y = 1.000000500.
//
// The parametrisations are the tidy ones. The cissoid is (sin²t, sin³t/cos t), and the
// cruciform is simply (sec t, csc t) — one branch per quadrant of t. Every one was verified
// against its own implicit equation before being drawn; the worst of the eight is 7e-14.
//
// manic examples/zoo-exponents-3.manic
title("Eight curves filed by where they run off to infinity");
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, "The denominator says where it leaves. The numerator, what it does at 0.", (540, 116), 22);
coords(k_agn, (400.00, 389.06), (-3.2000, 3.2000), (-2.6555, 3.7445), 35.0000, 35.0000, 1, 2.0, 1);
color(k_agn, dim); opacity(k_agn, 0.4);
coords(k_ser, (680.00, 370.00), (-3.2000, 3.2000), (-3.2000, 3.2000), 35.0000, 35.0000, 1, 2.0, 1);
color(k_ser, dim); opacity(k_ser, 0.4);
coords(k_tri, (270.00, 790.00), (-2.4853, 2.4853), (-2.4853, 2.4853), 47.4794, 47.4794, 1, 1.0, 1);
color(k_tri, dim); opacity(k_tri, 0.4);
line(v0_tri, (270.0, 672), (270.0, 908)); color(v0_tri, coral); dashed(v0_tri); opacity(v0_tri, 0.55);
coords(k_cmx, (463.12, 790.00), (-1.4092, 6.6792), (-4.0442, 4.0442), 29.1774, 29.1774, 1, 2.0, 1);
color(k_cmx, dim); opacity(k_cmx, 0.4);
line(v0_cmx, (492.3, 672), (492.3, 908)); color(v0_cmx, coral); dashed(v0_cmx); opacity(v0_cmx, 0.55);
coords(k_cis, (793.62, 790.00), (-2.8801, 3.8086), (-3.3444, 3.3444), 35.2834, 35.2834, 1, 2.0, 1);
color(k_cis, dim); opacity(k_cis, 0.4);
line(v0_cis, (828.9, 672), (828.9, 908)); color(v0_cis, coral); dashed(v0_cis); opacity(v0_cis, 0.55);
coords(k_duu, (270.00, 1200.00), (-4.1933, 4.1933), (-4.1933, 4.1933), 28.1402, 28.1402, 1, 2.0, 1);
color(k_duu, dim); opacity(k_duu, 0.4);
line(v0_duu, (241.9, 1082), (241.9, 1318)); color(v0_duu, coral); dashed(v0_duu); opacity(v0_duu, 0.55);
line(v1_duu, (298.1, 1082), (298.1, 1318)); color(v1_duu, coral); dashed(v1_duu); opacity(v1_duu, 0.55);
coords(k_pun, (540.00, 1200.00), (-4.0723, 4.0723), (-4.0723, 4.0723), 28.9762, 28.9762, 1, 2.0, 1);
color(k_pun, dim); opacity(k_pun, 0.4);
line(v0_pun, (511.0, 1082), (511.0, 1318)); color(v0_pun, coral); dashed(v0_pun); opacity(v0_pun, 0.55);
line(v1_pun, (569.0, 1082), (569.0, 1318)); color(v1_pun, coral); dashed(v1_pun); opacity(v1_pun, 0.55);
coords(k_cru, (810.00, 1200.00), (-3.7491, 3.7491), (-3.7491, 3.7491), 31.4743, 31.4743, 1, 2.0, 1);
color(k_cru, dim); opacity(k_cru, 0.4);
line(v0_cru, (778.5, 1082), (778.5, 1318)); color(v0_cru, coral); dashed(v0_cru); opacity(v0_cru, 0.55);
line(v1_cru, (841.5, 1082), (841.5, 1318)); color(v1_cru, coral); dashed(v1_cru); opacity(v1_cru, 0.55);
line(h0_cru, (692, 1231.5), (928, 1231.5)); color(h0_cru, coral); dashed(h0_cru); opacity(h0_cru, 0.55);
line(h1_cru, (692, 1168.5), (928, 1168.5)); color(h1_cru, coral); dashed(h1_cru); opacity(h1_cru, 0.55);
plot(agn0, (400.00, 389.06), 35.0000, 35.0000, "1/(1 + x*x)", (-3.2, 3.2));
color(agn0, gold); stroke(agn0, 4); untraced(agn0);
plot(ser0, (680.00, 370.00), 35.0000, 35.0000, "x/(1 + x*x)", (-3.2, 3.2));
color(ser0, gold); stroke(ser0, 4); untraced(ser0);
plot(tri0, (270.00, 790.00), 47.4794, 47.4794, "sqrt(max(x*x + 1/x, 0))", (0.24, 2.4));
color(tri0, mint); stroke(tri0, 4); untraced(tri0);
plot(tri1, (270.00, 790.00), 47.4794, 47.4794, "0 - sqrt(max(x*x + 1/x, 0))", (0.24, 2.4));
color(tri1, mint); stroke(tri1, 4); untraced(tri1);
plot(tri2, (270.00, 790.00), 47.4794, 47.4794, "sqrt(max(x*x + 1/x, 0))", (-2.4, -1.0));
color(tri2, mint); stroke(tri2, 4); untraced(tri2);
plot(tri3, (270.00, 790.00), 47.4794, 47.4794, "0 - sqrt(max(x*x + 1/x, 0))", (-2.4, -1.0));
color(tri3, mint); stroke(tri3, 4); untraced(tri3);
plot(cmx0, (463.12, 790.00), 29.1774, 29.1774, "x/sqrt(x - 1)", (1.07, 4.2));
color(cmx0, mint); stroke(cmx0, 4); untraced(cmx0);
plot(cmx1, (463.12, 790.00), 29.1774, 29.1774, "0 - x/sqrt(x - 1)", (1.07, 4.2));
color(cmx1, mint); stroke(cmx1, 4); untraced(cmx1);
param(cis0, (793.62, 790.00), 35.2834, 35.2834,
"sin(t)*sin(t)", "sin(t)*sin(t)*sin(t)/cos(t)", (-1.30000, 1.30000));
color(cis0, mint); stroke(cis0, 4); untraced(cis0);
param(duu0, (270.00, 1200.00), 28.1402, 28.1402,
"sin(t)", "1/cos(t)", (-1.33000, 1.33000));
color(duu0, violet); stroke(duu0, 4); untraced(duu0);
param(duu1, (270.00, 1200.00), 28.1402, 28.1402,
"sin(t)", "1/cos(t)", (1.81159, 4.47159));
color(duu1, violet); stroke(duu1, 4); untraced(duu1);
param(pun0, (540.00, 1200.00), 28.9762, 28.9762,
"sin(t)", "sin(t)/cos(t)", (-1.33000, 1.33000));
color(pun0, violet); stroke(pun0, 4); untraced(pun0);
param(pun1, (540.00, 1200.00), 28.9762, 28.9762,
"sin(t)", "0 - sin(t)/cos(t)", (-1.33000, 1.33000));
color(pun1, violet); stroke(pun1, 4); untraced(pun1);
param(cru0, (810.00, 1200.00), 31.4743, 31.4743,
"1/cos(t)", "1/sin(t)", (0.27000, 1.30000));
color(cru0, violet); stroke(cru0, 4); untraced(cru0);
param(cru1, (810.00, 1200.00), 31.4743, 31.4743,
"1/cos(t)", "1/sin(t)", (1.84159, 2.87159));
color(cru1, violet); stroke(cru1, 4); untraced(cru1);
param(cru2, (810.00, 1200.00), 31.4743, 31.4743,
"1/cos(t)", "1/sin(t)", (3.41159, 4.44159));
color(cru2, violet); stroke(cru2, 4); untraced(cru2);
param(cru3, (810.00, 1200.00), 31.4743, 31.4743,
"1/cos(t)", "1/sin(t)", (4.98319, 6.01319));
color(cru3, violet); stroke(cru3, 4); untraced(cru3);
text(n_agn, (400, 518), "witch of Agnesi"); size(n_agn, 20); color(n_agn, fg);
equation(q_agn, (400, 562), `y=\frac{1}{1+x^2}`, 26);
text(n_ser, (680, 518), "serpentine"); size(n_ser, 20); color(n_ser, fg);
equation(q_ser, (680, 562), `y=\frac{x}{1+x^2}`, 26);
text(n_tri, (270, 944), "trident of Newton"); size(n_tri, 20); color(n_tri, fg);
equation(q_tri, (270, 988), `y^2=\frac{x^3+1}{x}`, 26);
text(n_cmx, (540, 944), "mixed cubic"); size(n_cmx, 20); color(n_cmx, fg);
equation(q_cmx, (540, 988), `y^2=\frac{x^2}{x-1}`, 26);
circle(a_cmx, (463.1, 790.0), 6); color(a_cmx, cyan);
text(n_cis, (810, 944), "cissoid of Diocles"); size(n_cis, 20); color(n_cis, fg);
equation(q_cis, (810, 988), `y^2=\frac{x^3}{1-x}`, 26);
text(n_duu, (270, 1354), "double u"); size(n_duu, 20); color(n_duu, fg);
equation(q_duu, (270, 1398), `y^2=\frac{1}{1-x^2}`, 26);
text(n_pun, (540, 1354), "puntiforme"); size(n_pun, 20); color(n_pun, fg);
equation(q_pun, (540, 1398), `y^2=\frac{x^2}{1-x^2}`, 26);
text(n_cru, (810, 1354), "cruciform"); size(n_cru, 20); color(n_cru, fg);
equation(q_cru, (810, 1398), `y^2=\frac{x^2}{x^2-1}`, 26);
circle(a_cru, (810.0, 1200.0), 6); color(a_cru, cyan);
text(n1, (540, 1470),
"x²/(1 − x²) and x²/(x² − 1): the same fraction upside down, real on opposite sides of |x| = 1.");
size(n1, 20); color(n1, fg); wrap(n1, 980);
text(n2, (540, 1560),
"Same poles, three numerators: no root keeps |y| ≥ 1, an x² sends it through the origin, a flipped sign leaves only a point.");
size(n2, 19); color(n2, dim); wrap(n2, 980);
equation(eq, (540, 1700), `y^2=\frac{P(x)}{Q(x)}`, 46);
par {
draw(agn0, 3.2);
draw(ser0, 3.2);
seq { wait(2.6); draw(tri0, 3.2); }
seq { wait(2.6); draw(tri1, 3.2); }
seq { wait(2.6); draw(tri2, 3.2); }
seq { wait(2.6); draw(tri3, 3.2); }
seq { wait(4.4); draw(cmx0, 3.2); }
seq { wait(4.4); draw(cmx1, 3.2); }
seq { wait(6.2); draw(cis0, 3.2); }
seq { wait(8.0); draw(duu0, 3.2); }
seq { wait(8.0); draw(duu1, 3.2); }
seq { wait(9.8); draw(pun0, 3.2); }
seq { wait(9.8); draw(pun1, 3.2); }
seq { wait(11.4); draw(cru0, 3.2); }
seq { wait(11.4); draw(cru1, 3.2); }
seq { wait(11.4); draw(cru2, 3.2); }
seq { wait(11.4); draw(cru3, 3.2); }
}
wait(3.4);
zoo-exponents-4
The fourth of mathcurve’s tables, and the one where five of the eight turn out to be the same cubic: y² = x²·(a−x)/(b+x). Everything about the origin follows from ONE number, the ratio a/b the fraction takes at x = 0, since y ≈ ±√(a/b)·x there. Positive gives two real tangents — a NODE at ±arctan√(a/b); negative gives none, and yet (0,0) still satisfies the equation: an ACNODE. That sorts four of them and names two. The strophoid has ratio +1, tangents at ±45°, ninety degrees apart — which is why it is the RIGHT strophoid. The trisectrix of Maclaurin has ratio +3, tangents at ±60°, a hundred and twenty apart. The equilateral trefoil (−1) and the visiera (−2) get acnodes. One constant moved from 1 to 3 turns a right angle into a 120° one, and that is the difference between a strophoid and a curve that trisects an angle — the two sit side by side so you can see it. THE OBLIQUE ASYMPTOTES ARE WHERE “EQUILATERAL” COMES FROM: where the top outranks the bottom by two, the curve leaves along a slant, and both the trefoil and the Humbert cubic leave along y = ±x/√3 — thirty degrees (at x = −1000, Humbert gives 0.577350269 against 1/√3 = 0.577350269). Drawn dashed in coral with the vertical ones. The other three are the odd ones out: the Külp quartic’s denominator never vanishes so it has no asymptote at all, the cappa keeps the poles but puts x⁴ on top so it leaves the origin flat, and the bicorne is a fraction of a polynomial and a square root.
// zoo-exponents-4 — the fourth of mathcurve's tables, and the one where five of the eight
// turn out to be the same cubic. Their shared form is
//
// y² = x² · (a − x)/(b + x)
//
// and everything about the origin follows from ONE number, the ratio a/b that the fraction
// takes at x = 0. Near zero y ≈ ±√(a/b)·x, so:
//
// sign + two real tangents, a NODE, at ±arctan√(a/b)
// sign − no real tangent, and yet (0,0) satisfies the equation: an ACNODE
//
// which sorts four of them at a glance, and names two of them:
//
// strophoid x²(1−x)/(1+x) ratio +1 tangents ±45°, ninety degrees apart.
// That right angle is why it is the RIGHT
// strophoid.
// trisectrix x²(3−x)/(1+x) ratio +3 tangents ±60°, a hundred and twenty apart
// trefoil x²(x−1)/(3x+1) ratio −1 acnode
// visiera x²(2−x)/(x−1) ratio −2 acnode
//
// One constant moved from 1 to 3 turns a right angle into a 120° one, and that is the whole
// difference between a strophoid and a curve that trisects an angle.
//
// THE OBLIQUE ASYMPTOTES ARE WHERE "EQUILATERAL" COMES FROM. When the top outranks the bottom
// by two degrees the curve leaves along a slanted line rather than a vertical one. Both the
// trefoil and the Humbert cubic do, and both leave along y = ±x/√3 — thirty degrees, the
// equilateral angle. Checked: at x = −1000 the trefoil gives |y|/|x| = 0.577735 and Humbert
// 0.577350269, against 1/√3 = 0.577350269. They are drawn, dashed, in coral, along with the
// vertical asymptotes.
//
// The other three are the odd ones. y² = 1/(x² + 1) has a denominator that never vanishes, so
// the Külp quartic is bounded and asymptote-free, two arcs pressed toward y = 0. y² = x⁴/(1−x²)
// keeps the poles at ±1 but puts x⁴ on top, so the cappa leaves the origin flat instead of
// crossing. And the bicorne is not a fraction of polynomials at all but of a polynomial and a
// square root, ± in the denominator — two arcs that meet at (±1, 0) and make the two horns.
//
// Every parametrisation was verified against its own implicit equation before being drawn.
// The tidy ones are worth naming: the strophoid is (cos u, cos u · tan(u/2)), exact because
// (1−cos u)/(1+cos u) is tan²(u/2); the Külp quartic is (tan t, cos t); the cappa is
// (sin t, sin²t/cos t). Worst residual of the nine curves drawn: 1e-13.
//
// manic examples/zoo-exponents-4.manic
title("One ratio at the origin, and four curves sort themselves");
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, "One ratio at the origin decides the node — and its angle", (540, 116), 24);
coords(k_kul, (400.00, 360.00), (-3.0096, 3.0096), (-3.0096, 3.0096), 35.8855, 35.8855, 1, 2.0, 1);
color(k_kul, dim); opacity(k_kul, 0.4);
coords(k_cap, (680.00, 360.00), (-3.9548, 3.9548), (-3.9548, 3.9548), 27.3085, 27.3085, 1, 2.0, 1);
color(k_cap, dim); opacity(k_cap, 0.4);
line(v0_cap, (652.7, 252), (652.7, 468)); color(v0_cap, coral); dashed(v0_cap); opacity(v0_cap, 0.5);
line(v1_cap, (707.3, 252), (707.3, 468)); color(v1_cap, coral); dashed(v1_cap); opacity(v1_cap, 0.5);
coords(k_str, (169.58, 790.00), (-2.1921, 2.4030), (-2.2976, 2.2976), 51.3589, 51.3589, 1, 1.0, 1);
color(k_str, dim); opacity(k_str, 0.4);
line(v0_str, (118.2, 672), (118.2, 908)); color(v0_str, coral); dashed(v0_str); opacity(v0_str, 0.5);
coords(k_mac, (390.54, 790.00), (-3.2313, 5.3813), (-4.3063, 4.3063), 27.4018, 27.4018, 1, 2.0, 1);
color(k_mac, dim); opacity(k_mac, 0.4);
line(v0_mac, (363.1, 672), (363.1, 908)); color(v0_mac, coral); dashed(v0_mac); opacity(v0_mac, 0.5);
coords(k_trf, (665.00, 790.00), (-3.4000, 3.4000), (-3.4000, 3.4000), 34.7059, 34.7059, 1, 2.0, 1);
color(k_trf, dim); opacity(k_trf, 0.4);
line(v0_trf, (653.4, 672), (653.4, 908)); color(v0_trf, coral); dashed(v0_trf); opacity(v0_trf, 0.5);
line(o0p_trf, (547, 858.1), (783, 721.9)); color(o0p_trf, coral); dashed(o0p_trf); opacity(o0p_trf, 0.4);
line(o0m_trf, (547, 721.9), (783, 858.1)); color(o0m_trf, coral); dashed(o0m_trf); opacity(o0m_trf, 0.4);
coords(k_vis, (872.68, 790.00), (-3.2916, 6.3366), (-4.8141, 4.8141), 24.5115, 24.5115, 1, 2.0, 1);
color(k_vis, dim); opacity(k_vis, 0.4);
line(v0_vis, (897.2, 672), (897.2, 908)); color(v0_vis, coral); dashed(v0_vis); opacity(v0_vis, 0.5);
coords(k_bic, (400.00, 1269.00), (-1.0000, 1.0000), (-0.5000, 1.5000), 118.0000, 118.0000, 1, 0.5, 1);
color(k_bic, dim); opacity(k_bic, 0.4);
coords(k_hum, (680.00, 1210.00), (-2.8000, 2.8000), (-2.8000, 2.8000), 42.1429, 42.1429, 1, 2.0, 1);
color(k_hum, dim); opacity(k_hum, 0.4);
line(v0_hum, (680.0, 1092), (680.0, 1328)); color(v0_hum, coral); dashed(v0_hum); opacity(v0_hum, 0.5);
line(o0p_hum, (562, 1278.1), (798, 1141.9)); color(o0p_hum, coral); dashed(o0p_hum); opacity(o0p_hum, 0.4);
line(o0m_hum, (562, 1141.9), (798, 1278.1)); color(o0m_hum, coral); dashed(o0m_hum); opacity(o0m_hum, 0.4);
param(kul0, (400.00, 360.00), 35.8855, 35.8855,
"tan(t)", "cos(t)", (-1.25000, 1.25000));
color(kul0, gold); stroke(kul0, 4); untraced(kul0);
param(kul1, (400.00, 360.00), 35.8855, 35.8855,
"tan(t)", "0 - cos(t)", (-1.25000, 1.25000));
color(kul1, gold); stroke(kul1, 4); untraced(kul1);
param(cap0, (680.00, 360.00), 27.3085, 27.3085,
"sin(t)", "sin(t)*sin(t)/cos(t)", (-1.33000, 1.33000));
color(cap0, gold); stroke(cap0, 4); untraced(cap0);
param(cap1, (680.00, 360.00), 27.3085, 27.3085,
"sin(t)", "sin(t)*sin(t)/cos(t)", (1.81159, 4.47159));
color(cap1, gold); stroke(cap1, 4); untraced(cap1);
param(str0, (169.58, 790.00), 51.3589, 51.3589,
"cos(t)", "cos(t)*tan(t/2)", (-2.48000, 2.48000));
color(str0, mint); stroke(str0, 4); untraced(str0);
plot(mac0, (390.54, 790.00), 27.4018, 27.4018, "x*sqrt(max((3 - x)/(1 + x), 0))", (-0.85, 3.0));
color(mac0, mint); stroke(mac0, 4); untraced(mac0);
plot(mac1, (390.54, 790.00), 27.4018, 27.4018, "0 - x*sqrt(max((3 - x)/(1 + x), 0))", (-0.85, 3.0));
color(mac1, mint); stroke(mac1, 4); untraced(mac1);
plot(trf0, (665.00, 790.00), 34.7059, 34.7059, "abs(x)*sqrt(max((x - 1)/(3*x + 1), 0))", (1.0, 3.4));
color(trf0, cyan); stroke(trf0, 4); untraced(trf0);
plot(trf1, (665.00, 790.00), 34.7059, 34.7059, "0 - abs(x)*sqrt(max((x - 1)/(3*x + 1), 0))", (1.0, 3.4));
color(trf1, cyan); stroke(trf1, 4); untraced(trf1);
plot(trf2, (665.00, 790.00), 34.7059, 34.7059, "abs(x)*sqrt(max((x - 1)/(3*x + 1), 0))", (-3.4, -0.38));
color(trf2, cyan); stroke(trf2, 4); untraced(trf2);
plot(trf3, (665.00, 790.00), 34.7059, 34.7059, "0 - abs(x)*sqrt(max((x - 1)/(3*x + 1), 0))", (-3.4, -0.38));
color(trf3, cyan); stroke(trf3, 4); untraced(trf3);
plot(vis0, (872.68, 790.00), 24.5115, 24.5115, "x*sqrt(max((2 - x)/(x - 1), 0))", (1.045, 2.0));
color(vis0, cyan); stroke(vis0, 4); untraced(vis0);
plot(vis1, (872.68, 790.00), 24.5115, 24.5115, "0 - x*sqrt(max((2 - x)/(x - 1), 0))", (1.045, 2.0));
color(vis1, cyan); stroke(vis1, 4); untraced(vis1);
param(bic0, (400.00, 1269.00), 118.0000, 118.0000,
"sin(t)", "cos(t)*cos(t)/(2 + cos(t))", (-1.57070, 1.57070));
color(bic0, violet); stroke(bic0, 4); untraced(bic0);
param(bic1, (400.00, 1269.00), 118.0000, 118.0000,
"sin(t)", "cos(t)*cos(t)/(2 - cos(t))", (-1.57070, 1.57070));
color(bic1, violet); stroke(bic1, 4); untraced(bic1);
plot(hum0, (680.00, 1210.00), 42.1429, 42.1429, "sqrt(max((x*x*x - 1)/(3*x), 0))", (1.0, 2.8));
color(hum0, violet); stroke(hum0, 4); untraced(hum0);
plot(hum1, (680.00, 1210.00), 42.1429, 42.1429, "0 - sqrt(max((x*x*x - 1)/(3*x), 0))", (1.0, 2.8));
color(hum1, violet); stroke(hum1, 4); untraced(hum1);
plot(hum2, (680.00, 1210.00), 42.1429, 42.1429, "sqrt(max((x*x*x - 1)/(3*x), 0))", (-2.8, -0.05));
color(hum2, violet); stroke(hum2, 4); untraced(hum2);
plot(hum3, (680.00, 1210.00), 42.1429, 42.1429, "0 - sqrt(max((x*x*x - 1)/(3*x), 0))", (-2.8, -0.05));
color(hum3, violet); stroke(hum3, 4); untraced(hum3);
text(n_kul, (400, 504), "Külp quartic"); size(n_kul, 19); color(n_kul, fg);
equation(q_kul, (400, 550), `y^2=\frac{1}{x^2+1}`, 25);
text(n_cap, (680, 504), "cappa"); size(n_cap, 19); color(n_cap, fg);
equation(q_cap, (680, 550), `y^2=\frac{x^4}{1-x^2}`, 25);
text(n_str, (175, 944), "right strophoid"); size(n_str, 17); color(n_str, fg);
equation(q_str, (175, 990), `y^2=x^2\frac{1-x}{1+x}`, 25);
text(n_mac, (420, 944), "trisectrix of Maclaurin"); size(n_mac, 17); color(n_mac, fg);
equation(q_mac, (420, 990), `y^2=x^2\frac{3-x}{1+x}`, 25);
text(n_trf, (665, 944), "equilateral trefoil"); size(n_trf, 17); color(n_trf, fg);
equation(q_trf, (665, 990), `y^2=x^2\frac{x-1}{3x+1}`, 25);
circle(a_trf, (665.0, 790.0), 6); color(a_trf, magenta);
text(n_vis, (910, 944), "visiera"); size(n_vis, 17); color(n_vis, fg);
equation(q_vis, (910, 990), `y^2=x^2\frac{2-x}{x-1}`, 25);
circle(a_vis, (872.7, 790.0), 6); color(a_vis, magenta);
text(n_bic, (400, 1364), "bicorne"); size(n_bic, 19); color(n_bic, fg);
equation(q_bic, (400, 1410), `y=\frac{1-x^2}{2\pm\sqrt{1-x^2}}`, 25);
text(n_hum, (680, 1364), "Humbert cubic"); size(n_hum, 19); color(n_hum, fg);
equation(q_hum, (680, 1410), `y^2=\frac{x^3-1}{3x}`, 25);
text(n1, (540, 1500),
"y² = x²(a − x)/(b + x): at the origin y ≈ ±√(a/b)·x. Ratio +1 gives ±45°, +3 gives ±60°, negative gives no curve at all.");
size(n1, 19); color(n1, fg); wrap(n1, 990);
text(n2, (540, 1600),
"Where the top outranks the bottom by two, the curve leaves along a slant: both of these leave at ±30°.");
size(n2, 19); color(n2, dim); wrap(n2, 990);
equation(eq, (540, 1730), `y^2=x^2\,\frac{a-x}{b+x}`, 44);
par {
draw(kul0, 3.2);
draw(kul1, 3.2);
seq { wait(1.7); draw(cap0, 3.2); }
seq { wait(1.7); draw(cap1, 3.2); }
seq { wait(3.4); draw(str0, 3.2); }
seq { wait(5.1); draw(mac0, 3.2); }
seq { wait(5.1); draw(mac1, 3.2); }
seq { wait(6.8); draw(trf0, 3.2); }
seq { wait(6.8); draw(trf1, 3.2); }
seq { wait(6.8); draw(trf2, 3.2); }
seq { wait(6.8); draw(trf3, 3.2); }
seq { wait(8.5); draw(vis0, 3.2); }
seq { wait(8.5); draw(vis1, 3.2); }
seq { wait(10.2); draw(bic0, 3.2); }
seq { wait(10.2); draw(bic1, 3.2); }
seq { wait(11.8); draw(hum0, 3.2); }
seq { wait(11.8); draw(hum1, 3.2); }
seq { wait(11.8); draw(hum2, 3.2); }
seq { wait(11.8); draw(hum3, 3.2); }
}
wait(3.4);
zoo-polar
The same herbarium filed in POLAR, and the batch that closes the loop. The whole table is one family, ρ = cosⁿθ and ρ = 1/cosⁿθ — and the second is the INVERSE of the first in the unit circle, because inversion is exactly ρ ↦ 1/ρ. The dashed coral circle in every cell is that circle, and each column is one inverse pair: a circle through the origin ↔ the line x = 1 (the classical fact), the double egg ↔ the campyle, the simple folium ↔ the duplicating cubic, the dipole ↔ a Külp-like quartic. The top row lives inside the circle and the bottom outside, which is inversion made visible. AND THEY ARE CURVES THIS SERIES ALREADY DREW: checked rather than assumed, ρ = cos²θ IS batch one’s double egg y² = x^(4/3) − x² (2e-16), ρ = cos³θ IS its simple folium (2e-16), ρ² = cos θ IS its dipole (3e-16), ρ = 1/cos²θ IS batch two’s campyle (3e-14) and ρ = 1/cos³θ its duplicating cubic (2e-13). Which is the point of doing polar last: the fractional exponents that made the first batch look like a zoo — 2/3, 4/3, 3/2 — are cos²θ, cos³θ and √(cos θ) seen in Cartesian coordinates, and the reciprocal pairs batch two found by flipping a sign are inversions. One family, read four ways. The last column is the pair that does not pair: the conic in focal form (at e = 0.6, as in orbital-eccentricity) and ρ³ = 1/cos θ, which is x(x²+y²) = 1.
// zoo-polar — the same herbarium, filed in POLAR, and the batch that closes the loop. The
// whole table is one family:
//
// ρ = cos^n θ and ρ = 1/cos^n θ
//
// and the second is the INVERSE OF THE FIRST IN THE UNIT CIRCLE, because inversion is exactly
// ρ ↦ 1/ρ. The dashed coral circle in every cell is that circle. Each column here is one
// inverse pair, top and bottom — and inversion renames them:
//
// ρ = cos θ a CIRCLE through the origin ↦ ρ = 1/cos θ the LINE x = 1
// ρ = cos²θ the DOUBLE EGG ↦ ρ = 1/cos²θ the CAMPYLE
// ρ = cos³θ the SIMPLE FOLIUM ↦ ρ = 1/cos³θ the DUPLICATING CUBIC
// ρ² = cos θ the DIPOLE CURVE ↦ ρ² = 1/cos θ a Külp-like quartic
//
// The first is the classical fact that inversion carries circles through the centre to lines.
// The rest are the same fact wearing other names.
//
// AND THEY ARE CURVES THIS SERIES HAS ALREADY DRAWN. Every one of the left column and two of
// the right are in the Cartesian batches, and the polar form is the same curve — checked, not
// assumed:
//
// ρ = cos²θ is y² = x^(4/3) − x² the double egg 2e-16 (batch 1)
// ρ = cos³θ is y² = x^(3/2) − x² the simple folium 2e-16 (batch 1)
// ρ² = cos θ is y² = x^(2/3) − x² the dipole curve 3e-16 (batch 1)
// ρ = 1/cos²θ is y² = x²(x² − 1) the campyle 3e-14 (batch 2)
// ρ = 1/cos³θ is y² = x²(x − 1) the duplicating cubic 2e-13 (batch 1)
//
// Which is the point of doing the polar table last. The fractional exponents that made batch
// one look like a zoo — 2/3, 4/3, 3/2 — are just cos²θ, cos³θ and √(cos θ) seen in Cartesian
// coordinates, and the reciprocal pairs that batch two found by flipping a sign are
// inversions. One family, read four ways.
//
// The last column is the pair that does not pair. ρ = p/(1 + e·cos θ) is the conic — the focal
// form used in examples/orbital-eccentricity.manic, drawn here at e = 0.6 — and ρ³ = 1/cos θ
// is x(x² + y²) = 1, a cubic with a vertical asymptote which mathcurve files next to Agnesi's
// without their being the same curve. Both implicit forms are checked to 1e-15.
//
// Drawn with `param` against the polar form directly, and every one verified against its own
// implicit Cartesian equation before being drawn.
//
// manic examples/zoo-polar.manic
title("Inversion renames the curve: the polar table");
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, "Each column is one curve and its inverse in the dashed circle", (540, 116), 23);
coords(k_cir, (130.00, 400.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 95.0000, 95.0000, 1, 0.5, 0);
color(k_cir, dim); opacity(k_cir, 0.35);
circle(u_cir, (130.00, 400.00), 95.00); outlined(u_cir); outline(u_cir, coral); stroke(u_cir, 2); dashed(u_cir); opacity(u_cir, 0.4);
coords(k_deg, (340.00, 400.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 95.0000, 95.0000, 1, 0.5, 0);
color(k_deg, dim); opacity(k_deg, 0.35);
circle(u_deg, (340.00, 400.00), 95.00); outlined(u_deg); outline(u_deg, coral); stroke(u_deg, 2); dashed(u_deg); opacity(u_deg, 0.4);
coords(k_fol, (550.00, 400.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 95.0000, 95.0000, 1, 0.5, 0);
color(k_fol, dim); opacity(k_fol, 0.35);
circle(u_fol, (550.00, 400.00), 95.00); outlined(u_fol); outline(u_fol, coral); stroke(u_fol, 2); dashed(u_fol); opacity(u_fol, 0.4);
coords(k_dip, (760.00, 400.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 95.0000, 95.0000, 1, 0.5, 0);
color(k_dip, dim); opacity(k_dip, 0.35);
circle(u_dip, (760.00, 400.00), 95.00); outlined(u_dip); outline(u_dip, coral); stroke(u_dip, 2); dashed(u_dip); opacity(u_dip, 0.4);
coords(k_con, (989.00, 400.00), (-1.5000, 1.0000), (-1.2500, 1.2500), 76.0000, 76.0000, 1, 0.5, 0);
color(k_con, dim); opacity(k_con, 0.35);
circle(u_con, (989.00, 400.00), 76.00); outlined(u_con); outline(u_con, coral); stroke(u_con, 2); dashed(u_con); opacity(u_con, 0.4);
coords(k_lin, (130.00, 900.00), (-2.2345, 2.2345), (-2.2345, 2.2345), 42.5152, 42.5152, 1, 1.0, 0);
color(k_lin, dim); opacity(k_lin, 0.35);
circle(u_lin, (130.00, 900.00), 42.52); outlined(u_lin); outline(u_lin, coral); stroke(u_lin, 2); dashed(u_lin); opacity(u_lin, 0.4);
coords(k_kam, (340.00, 900.00), (-5.7490, 5.7490), (-5.7490, 5.7490), 16.5246, 16.5246, 1, 5.0, 0);
color(k_kam, dim); opacity(k_kam, 0.35);
circle(u_kam, (340.00, 900.00), 16.52); outlined(u_kam); outline(u_kam, coral); stroke(u_kam, 2); dashed(u_kam); opacity(u_kam, 0.4);
coords(k_dup, (529.50, 900.00), (-5.5219, 8.5611), (-7.0415, 7.0415), 13.4914, 13.4914, 1, 5.0, 0);
color(k_dup, dim); opacity(k_dup, 0.35);
circle(u_dup, (529.50, 900.00), 13.49); outlined(u_dup); outline(u_dup, coral); stroke(u_dup, 2); dashed(u_dup); opacity(u_dup, 0.4);
coords(k_kul, (760.00, 900.00), (-1.4281, 1.4281), (-1.4281, 1.4281), 66.5203, 66.5203, 1, 1.0, 0);
color(k_kul, dim); opacity(k_kul, 0.35);
circle(u_kul, (760.00, 900.00), 66.52); outlined(u_kul); outline(u_kul, coral); stroke(u_kul, 2); dashed(u_kul); opacity(u_kul, 0.4);
coords(k_agn, (970.00, 900.00), (-1.4954, 1.4954), (-1.4954, 1.4954), 63.5262, 63.5262, 1, 1.0, 0);
color(k_agn, dim); opacity(k_agn, 0.35);
circle(u_agn, (970.00, 900.00), 63.53); outlined(u_agn); outline(u_agn, coral); stroke(u_agn, 2); dashed(u_agn); opacity(u_agn, 0.4);
param(cir0, (130.00, 400.00), 95.0000, 95.0000,
"cos(t)*cos(t)", "cos(t)*sin(t)", (0.0, 3.141593));
color(cir0, gold); stroke(cir0, 4); untraced(cir0);
param(deg0, (340.00, 400.00), 95.0000, 95.0000,
"cos(t)*cos(t)*cos(t)", "cos(t)*cos(t)*sin(t)", (0.0, 6.283185));
color(deg0, mint); stroke(deg0, 4); untraced(deg0);
param(fol0, (550.00, 400.00), 95.0000, 95.0000,
"cos(t)*cos(t)*cos(t)*cos(t)", "cos(t)*cos(t)*cos(t)*sin(t)", (-1.5707, 1.5707));
color(fol0, cyan); stroke(fol0, 4); untraced(fol0);
param(dip0, (760.00, 400.00), 95.0000, 95.0000,
"sqrt(max(cos(t), 0))*cos(t)", "sqrt(max(cos(t), 0))*sin(t)", (-1.5707, 1.5707));
color(dip0, violet); stroke(dip0, 4); untraced(dip0);
param(dip1, (760.00, 400.00), 95.0000, 95.0000,
"0 - sqrt(max(cos(t), 0))*cos(t)", "0 - sqrt(max(cos(t), 0))*sin(t)", (-1.5707, 1.5707));
color(dip1, violet); stroke(dip1, 4); untraced(dip1);
param(con0, (989.00, 400.00), 76.0000, 76.0000,
"0.6*cos(t)/(1 + 0.6*cos(t))", "0.6*sin(t)/(1 + 0.6*cos(t))", (0.0, 6.283185));
color(con0, coral); stroke(con0, 4); untraced(con0);
param(lin0, (130.00, 900.00), 42.5152, 42.5152,
"1", "tan(t)", (-1.15, 1.15));
color(lin0, gold); stroke(lin0, 4); untraced(lin0);
param(kam0, (340.00, 900.00), 16.5246, 16.5246,
"1/cos(t)", "sin(t)/(cos(t)*cos(t))", (-1.16, 1.16));
color(kam0, mint); stroke(kam0, 4); untraced(kam0);
param(kam1, (340.00, 900.00), 16.5246, 16.5246,
"1/cos(t)", "sin(t)/(cos(t)*cos(t))", (1.981593, 4.3015929999999996));
color(kam1, mint); stroke(kam1, 4); untraced(kam1);
param(dup0, (529.50, 900.00), 13.4914, 13.4914,
"1/(cos(t)*cos(t))", "sin(t)/(cos(t)*cos(t)*cos(t))", (-1.05, 1.05));
color(dup0, cyan); stroke(dup0, 4); untraced(dup0);
param(kul0, (760.00, 900.00), 66.5203, 66.5203,
"cos(t)/sqrt(cos(t))", "sin(t)/sqrt(cos(t))", (-1.15, 1.15));
color(kul0, violet); stroke(kul0, 4); untraced(kul0);
param(kul1, (760.00, 900.00), 66.5203, 66.5203,
"0 - cos(t)/sqrt(cos(t))", "0 - sin(t)/sqrt(cos(t))", (-1.15, 1.15));
color(kul1, violet); stroke(kul1, 4); untraced(kul1);
param(agn0, (970.00, 900.00), 63.5262, 63.5262,
"exp(ln(1/cos(t))/3)*cos(t)", "exp(ln(1/cos(t))/3)*sin(t)", (-1.3, 1.3));
color(agn0, coral); stroke(agn0, 4); untraced(agn0);
param(agn1, (970.00, 900.00), 63.5262, 63.5262,
"0 - exp(ln(1/cos(t))/3)*cos(t)", "0 - exp(ln(1/cos(t))/3)*sin(t)", (-1.3, 1.3));
color(agn1, coral); stroke(agn1, 4); untraced(agn1);
text(n_cir, (130, 525), "circle"); size(n_cir, 16); color(n_cir, fg);
equation(q_cir, (130, 567), `\rho=\cos\theta`, 24);
text(n_deg, (340, 525), "double egg"); size(n_deg, 16); color(n_deg, fg);
equation(q_deg, (340, 567), `\rho=\cos^2\theta`, 24);
text(n_fol, (550, 525), "simple folium"); size(n_fol, 16); color(n_fol, fg);
equation(q_fol, (550, 567), `\rho=\cos^3\theta`, 24);
text(n_dip, (760, 525), "dipole curve"); size(n_dip, 16); color(n_dip, fg);
equation(q_dip, (760, 567), `\rho^2=\cos\theta`, 24);
text(n_con, (970, 525), "conic (e = 0.6)"); size(n_con, 16); color(n_con, fg);
equation(q_con, (970, 567), `\rho=\frac{p}{1+e\cos\theta}`, 24);
text(n_lin, (130, 1025), "line"); size(n_lin, 16); color(n_lin, fg);
equation(q_lin, (130, 1067), `\rho=\frac{1}{\cos\theta}`, 24);
text(n_kam, (340, 1025), "campyle"); size(n_kam, 16); color(n_kam, fg);
equation(q_kam, (340, 1067), `\rho=\frac{1}{\cos^2\theta}`, 24);
text(n_dup, (550, 1025), "duplicating cubic"); size(n_dup, 16); color(n_dup, fg);
equation(q_dup, (550, 1067), `\rho=\frac{1}{\cos^3\theta}`, 24);
text(n_kul, (760, 1025), "cf. Külp quartic"); size(n_kul, 16); color(n_kul, fg);
equation(q_kul, (760, 1067), `\rho^2=\frac{1}{\cos\theta}`, 24);
text(n_agn, (970, 1025), "cf. Agnesi cubic"); size(n_agn, 16); color(n_agn, fg);
equation(q_agn, (970, 1067), `\rho^3=\frac{1}{\cos\theta}`, 24);
text(n1, (540, 1185),
"ρ ↦ 1/ρ is inversion in the coral circle. It sends a circle through the origin to a line — and every column below is that same fact under another name.");
size(n1, 19); color(n1, fg); wrap(n1, 990);
text(n2, (540, 1315),
"The left column is batch one's fractional exponents: cos²θ is y² = x^(4/3) − x², cos³θ is x^(3/2) − x², √(cos θ) is x^(2/3) − x². One family, read twice.");
size(n2, 18); color(n2, dim); wrap(n2, 990);
equation(eq, (540, 1490), `\rho=\cos^{n}\theta\;\longleftrightarrow\;\rho=\frac{1}{\cos^{n}\theta}`, 40);
par {
draw(cir0, 3.0);
seq { wait(1.5); draw(deg0, 3.0); }
seq { wait(3.0); draw(fol0, 3.0); }
seq { wait(4.5); draw(dip0, 3.0); }
seq { wait(4.5); draw(dip1, 3.0); }
seq { wait(6.0); draw(con0, 3.0); }
seq { wait(7.5); draw(lin0, 3.0); }
seq { wait(9.0); draw(kam0, 3.0); }
seq { wait(9.0); draw(kam1, 3.0); }
seq { wait(10.5); draw(dup0, 3.0); }
seq { wait(12.0); draw(kul0, 3.0); }
seq { wait(12.0); draw(kul1, 3.0); }
seq { wait(13.0); draw(agn0, 3.0); }
seq { wait(13.0); draw(agn1, 3.0); }
}
wait(3.4);
zoo-polar-2
The second polar table, and two operations rather than one. The first four columns are pairs, each one curve acted on. INVERSION, ρ ↦ 1/ρ: ρ = cosθ/sin²θ is exactly y² = x, a PARABOLA, and ρ = sin²θ/cosθ is exactly y² = x³/(1−x), the CISSOID OF DIOCLES — so the cissoid is the inverse of a parabola in a circle at its vertex, one reciprocal apart. Likewise the BIFOLIUM ρ = cosθ·sin²θ and the MIXED CUBIC ρ = 1/(cosθ·sin²θ). SQUARE ROOT, ρ ↦ √ρ: the KAPPA ρ = tanθ becomes the RIGHT SERPENTINE ρ² = tanθ, and the WINDMILL ρ = tan2θ becomes the SWASTIKA ρ² = tan2θ — a square root does not invert a curve, it pulls everything toward the unit circle, which is why the windmill’s straight arms come back bent. AND FOUR OF THESE THE SERIES ALREADY DREW in Cartesian, checked not assumed: ρ = sin²θ/cos³θ IS y² = x³ (batch 1, 6e-14), ρ = sin²θ/cosθ IS y² = x³/(1−x) (batch 3, 7e-15), ρ = 1/(cosθsin²θ) IS y² = x²/(x−1) (batch 3, 3e-14), and ρ = tanθ IS y² = x⁴/(1−x²) (batch 4, 2e-14) — so the kappa of the polar table and the cappa of the Cartesian one are one curve spelt twice. The quadrifolium is the odd one and the simplest: ρ = cos 2θ, four petals, 6e-16.
// zoo-polar-2 — the second polar table, and two operations rather than one. The first four
// columns are pairs, and each pair is one curve acted on:
//
// INVERSION, ρ ↦ 1/ρ
// ρ = cos θ/sin²θ is exactly y² = x, a PARABOLA
// ρ = sin²θ/cos θ is exactly y² = x³/(1 − x), the CISSOID OF DIOCLES
// so the cissoid is the inverse of a parabola in a circle at its vertex — which is how
// Diocles' curve is usually introduced, and here it is one reciprocal apart.
//
// ρ = cos θ·sin²θ the BIFOLIUM, (x² + y²)² = x·y²
// ρ = 1/(cos θ·sin²θ) the MIXED CUBIC, y² = x²/(x − 1)
//
// SQUARE ROOT, ρ ↦ √ρ
// ρ = tan θ the KAPPA → ρ² = tan θ the RIGHT SERPENTINE
// ρ = tan 2θ the WINDMILL → ρ² = tan 2θ the SWASTIKA
//
// Taking a square root does not invert a curve; it pulls everything toward the unit circle,
// which is why the four arms of the windmill come back as the four bent arms of a swastika.
//
// AND FOUR OF THEM THIS SERIES HAS ALREADY DRAWN, in Cartesian coordinates, in three earlier
// batches. Checked rather than assumed:
//
// ρ = sin²θ/cos³θ is y² = x³ the semicubical parabola 6e-14 (batch 1)
// ρ = sin²θ/cos θ is y² = x³/(1 − x) the cissoid of Diocles 7e-15 (batch 3)
// ρ = 1/(cos θ sin²θ) is y² = x²/(x − 1) the mixed cubic 3e-14 (batch 3)
// ρ = tan θ is y² = x⁴/(1 − x²) the cappa 2e-14 (batch 4)
//
// So the kappa of the polar table and the cappa of the Cartesian one are the same curve under
// two spellings, and the semicubical parabola — the first cusp this series drew — is a ratio
// of a sine square to a cosine cube.
//
// The quadrifolium is the odd one and the simplest: ρ = cos 2θ, four petals, (x²+y²)³ =
// (x²−y²)², residual 6e-16. Every curve here was verified against its own implicit Cartesian
// equation before it was drawn; the worst of the nine with a closed form quoted is 7e-14.
//
// manic examples/zoo-polar-2.manic
title("Two operations on one family: invert it, or take its square root");
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, "Invert a curve, or take its square root — the columns are the pairs", (540, 116), 22);
coords(k_par, (35.00, 410.18), (0.0000, 5.5943), (-2.7919, 2.8024), 33.9633, 33.9633, 1, 2.0, 0);
color(k_par, dim); opacity(k_par, 0.35);
coords(k_bif, (303.43, 410.00), (-0.1998, 0.4498), (-0.3248, 0.3248), 292.5241, 292.5241, 1, 0.25, 0);
color(k_bif, dim); opacity(k_bif, 0.35);
coords(k_kap, (550.00, 410.00), (-3.2011, 3.2011), (-3.2011, 3.2011), 29.6776, 29.6776, 1, 2.0, 0);
color(k_kap, dim); opacity(k_kap, 0.35);
coords(k_win, (760.00, 410.00), (-4.4345, 4.4345), (-4.4345, 4.4345), 21.4231, 21.4231, 1, 2.0, 0);
color(k_win, dim); opacity(k_win, 0.35);
coords(k_scp, (941.48, 410.00), (-3.2313, 6.0043), (-4.6178, 4.6178), 20.5726, 20.5726, 1, 2.0, 0);
color(k_scp, dim); opacity(k_scp, 0.35);
coords(k_cis, (112.62, 910.00), (-1.9691, 2.8510), (-2.4100, 2.4100), 39.4185, 39.4185, 1, 1.0, 0);
color(k_cis, dim); opacity(k_cis, 0.35);
coords(k_mix, (214.86, 910.00), (1.1032, 8.0582), (-3.4775, 3.4775), 27.3184, 27.3184, 1, 2.0, 0);
color(k_mix, dim); opacity(k_mix, 0.35);
coords(k_ser, (550.00, 910.00), (-3.1285, 3.1285), (-3.1285, 3.1285), 30.3664, 30.3664, 1, 2.0, 0);
color(k_ser, dim); opacity(k_ser, 0.35);
coords(k_swa, (760.00, 910.00), (-2.4474, 2.4474), (-2.4474, 2.4474), 38.8171, 38.8171, 1, 1.0, 0);
color(k_swa, dim); opacity(k_swa, 0.35);
coords(k_qua, (970.00, 910.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 95.0000, 95.0000, 1, 0.5, 0);
color(k_qua, dim); opacity(k_qua, 0.35);
param(par0, (35.00, 410.18), 33.9633, 33.9633,
"cos(t)*cos(t)/(sin(t)*sin(t))", "cos(t)/sin(t)", (0.4, 2.74));
color(par0, gold); stroke(par0, 4); untraced(par0);
param(bif0, (303.43, 410.00), 292.5241, 292.5241,
"cos(t)*cos(t)*sin(t)*sin(t)", "cos(t)*sin(t)*sin(t)*sin(t)", (0.0, 3.141593));
color(bif0, mint); stroke(bif0, 4); untraced(bif0);
param(kap0, (550.00, 410.00), 29.6776, 29.6776,
"sin(t)", "sin(t)*sin(t)/cos(t)", (-1.28, 1.28));
color(kap0, cyan); stroke(kap0, 4); untraced(kap0);
param(kap1, (550.00, 410.00), 29.6776, 29.6776,
"sin(t)", "sin(t)*sin(t)/cos(t)", (1.8616, 4.4216));
color(kap1, cyan); stroke(kap1, 4); untraced(kap1);
param(win0, (760.00, 410.00), 21.4231, 21.4231,
"tan(2*t)*cos(t)", "tan(2*t)*sin(t)", (-0.7, 0.7));
color(win0, violet); stroke(win0, 4); untraced(win0);
param(win1, (760.00, 410.00), 21.4231, 21.4231,
"tan(2*t)*cos(t)", "tan(2*t)*sin(t)", (0.8708, 2.2708));
color(win1, violet); stroke(win1, 4); untraced(win1);
param(win2, (760.00, 410.00), 21.4231, 21.4231,
"tan(2*t)*cos(t)", "tan(2*t)*sin(t)", (2.4416, 3.8416));
color(win2, violet); stroke(win2, 4); untraced(win2);
param(win3, (760.00, 410.00), 21.4231, 21.4231,
"tan(2*t)*cos(t)", "tan(2*t)*sin(t)", (4.0124, 5.4124));
color(win3, violet); stroke(win3, 4); untraced(win3);
param(scp0, (941.48, 410.00), 20.5726, 20.5726,
"tan(t)*tan(t)", "tan(t)*tan(t)*tan(t)", (-1.03, 1.03));
color(scp0, coral); stroke(scp0, 4); untraced(scp0);
param(cis0, (112.62, 910.00), 39.4185, 39.4185,
"sin(t)*sin(t)", "sin(t)*sin(t)*sin(t)/cos(t)", (-1.22, 1.22));
color(cis0, gold); stroke(cis0, 4); untraced(cis0);
param(mix0, (214.86, 910.00), 27.3184, 27.3184,
"1/(sin(t)*sin(t))", "1/(cos(t)*sin(t))", (0.36, 1.26));
color(mix0, mint); stroke(mix0, 4); untraced(mix0);
param(mix1, (214.86, 910.00), 27.3184, 27.3184,
"1/(sin(t)*sin(t))", "1/(cos(t)*sin(t))", (1.8816, 2.7816));
color(mix1, mint); stroke(mix1, 4); untraced(mix1);
param(ser0, (550.00, 910.00), 30.3664, 30.3664,
"sqrt(max(tan(t), 0))*cos(t)", "sqrt(max(tan(t), 0))*sin(t)", (0.01, 1.47));
color(ser0, cyan); stroke(ser0, 4); untraced(ser0);
param(ser1, (550.00, 910.00), 30.3664, 30.3664,
"0 - sqrt(max(tan(t), 0))*cos(t)", "0 - sqrt(max(tan(t), 0))*sin(t)", (0.01, 1.47));
color(ser1, cyan); stroke(ser1, 4); untraced(ser1);
param(swa0, (760.00, 910.00), 38.8171, 38.8171,
"sqrt(max(tan(2*t), 0))*cos(t)", "sqrt(max(tan(2*t), 0))*sin(t)", (0.01, 0.74));
color(swa0, violet); stroke(swa0, 4); untraced(swa0);
param(swa1, (760.00, 910.00), 38.8171, 38.8171,
"0 - sqrt(max(tan(2*t), 0))*cos(t)", "0 - sqrt(max(tan(2*t), 0))*sin(t)", (0.01, 0.74));
color(swa1, violet); stroke(swa1, 4); untraced(swa1);
param(swa2, (760.00, 910.00), 38.8171, 38.8171,
"sqrt(max(tan(2*t), 0))*cos(t)", "sqrt(max(tan(2*t), 0))*sin(t)", (1.5808, 2.3108));
color(swa2, violet); stroke(swa2, 4); untraced(swa2);
param(swa3, (760.00, 910.00), 38.8171, 38.8171,
"0 - sqrt(max(tan(2*t), 0))*cos(t)", "0 - sqrt(max(tan(2*t), 0))*sin(t)", (1.5808, 2.3108));
color(swa3, violet); stroke(swa3, 4); untraced(swa3);
param(qua0, (970.00, 910.00), 95.0000, 95.0000,
"cos(2*t)*cos(t)", "cos(2*t)*sin(t)", (0.0, 6.283185));
color(qua0, coral); stroke(qua0, 4); untraced(qua0);
text(n_par, (130, 535), "parabola"); size(n_par, 15); color(n_par, fg);
equation(q_par, (130, 577), `\rho=\frac{\cos\theta}{\sin^2\theta}`, 23);
text(n_bif, (340, 535), "bifolium"); size(n_bif, 15); color(n_bif, fg);
equation(q_bif, (340, 577), `\rho=\cos\theta\sin^2\theta`, 23);
text(n_kap, (550, 535), "kappa"); size(n_kap, 15); color(n_kap, fg);
equation(q_kap, (550, 577), `\rho=\tan\theta`, 23);
text(n_win, (760, 535), "windmill"); size(n_win, 15); color(n_win, fg);
equation(q_win, (760, 577), `\rho=\tan 2\theta`, 23);
text(n_scp, (970, 535), "semicubical parabola"); size(n_scp, 15); color(n_scp, fg);
equation(q_scp, (970, 577), `\rho=\frac{\sin^2\theta}{\cos^3\theta}`, 23);
text(n_cis, (130, 1035), "cissoid of Diocles"); size(n_cis, 15); color(n_cis, fg);
equation(q_cis, (130, 1077), `\rho=\frac{\sin^2\theta}{\cos\theta}`, 23);
text(n_mix, (340, 1035), "mixed cubic"); size(n_mix, 15); color(n_mix, fg);
equation(q_mix, (340, 1077), `\rho=\frac{1}{\cos\theta\sin^2\theta}`, 23);
text(n_ser, (550, 1035), "right serpentine"); size(n_ser, 15); color(n_ser, fg);
equation(q_ser, (550, 1077), `\rho^2=\tan\theta`, 23);
text(n_swa, (760, 1035), "swastika"); size(n_swa, 15); color(n_swa, fg);
equation(q_swa, (760, 1077), `\rho^2=\tan 2\theta`, 23);
text(n_qua, (970, 1035), "quadrifolium"); size(n_qua, 15); color(n_qua, fg);
equation(q_qua, (970, 1077), `\rho=\cos 2\theta`, 23);
text(r_130, (130, 690), "inverses"); size(r_130, 16); color(r_130, coral);
text(r_340, (340, 690), "inverses"); size(r_340, 16); color(r_340, coral);
text(r_550, (550, 690), "square root"); size(r_550, 16); color(r_550, coral);
text(r_760, (760, 690), "square root"); size(r_760, 16); color(r_760, coral);
text(n1, (540, 1180),
"ρ ↦ 1/ρ turns a parabola into the cissoid of Diocles. ρ ↦ √ρ turns the windmill into the swastika.");
size(n1, 19); color(n1, fg); wrap(n1, 990);
text(n2, (540, 1290),
"The kappa here and the cappa of the Cartesian table are one curve, spelt twice: ρ = tan θ is y² = x⁴/(1 − x²).");
size(n2, 18); color(n2, dim); wrap(n2, 990);
equation(eq, (540, 1450), `\rho\mapsto\frac{1}{\rho}\qquad\rho\mapsto\sqrt{\rho}`, 40);
par {
draw(par0, 3.0);
seq { wait(1.5); draw(bif0, 3.0); }
seq { wait(3.0); draw(kap0, 3.0); }
seq { wait(3.0); draw(kap1, 3.0); }
seq { wait(4.5); draw(win0, 3.0); }
seq { wait(4.5); draw(win1, 3.0); }
seq { wait(4.5); draw(win2, 3.0); }
seq { wait(4.5); draw(win3, 3.0); }
seq { wait(6.0); draw(scp0, 3.0); }
seq { wait(7.5); draw(cis0, 3.0); }
seq { wait(9.0); draw(mix0, 3.0); }
seq { wait(9.0); draw(mix1, 3.0); }
seq { wait(10.5); draw(ser0, 3.0); }
seq { wait(10.5); draw(ser1, 3.0); }
seq { wait(12.0); draw(swa0, 3.0); }
seq { wait(12.0); draw(swa1, 3.0); }
seq { wait(12.0); draw(swa2, 3.0); }
seq { wait(12.0); draw(swa3, 3.0); }
seq { wait(13.2); draw(qua0, 3.0); }
}
wait(3.4);
zoo-polar-3
Multiple angles, and the sharpest inversion in the herbarium. ρ² = 1/cos 2θ is EXACTLY x² − y² = 1, a rectangular HYPERBOLA (1e-15); ρ² = cos 2θ is EXACTLY (x²+y²)² = x² − y², the LEMNISCATE OF BERNOULLI (4e-16). One reciprocal apart — so the lemniscate IS the inverse of the hyperbola, which is the cleanest statement the whole series has to make and the reason Bernoulli’s curve turns up wherever a hyperbola does. Two more columns are inversions too: the regular TRIFOLIUM ρ = cos 3θ against the EQUILATERAL TREFOIL ρ = 1/cos 3θ — and that trefoil is exactly y² = x²(x−1)/(3x+1), the curve zoo-exponents-4 drew from the Cartesian table (4e-15), its ±30° asymptotes being the three directions cos 3θ vanishes in; and the FOLIUM OF DÜRER ρ = cos(θ/2), which needs θ over 4π to close because a half angle halves the frequency, against the TRISECTRIX OF DELANGE. THE CRUCIFORM IS THE SAME CURVE TWICE: ρ = 1/cos 2θ gives (x²−y²)² = x²+y², and turning it 45° gives 4X²Y² = X²+Y² — zoo-exponents-3’s cruciform, scaled (7e-14). The two tables disagreed only about which way up to draw it. The rest is what a multiple angle buys: ρ² = 1/cos 4θ is the MALTESE CROSS (doubling the angle doubles the arms), ρ² = cos 3θ is KIEPERT’S CURVE, and ρ = cos²(θ/2) is the CARDIOID, because cos²(θ/2) is (1 + cos θ)/2 exactly.
// zoo-polar-3 — multiple angles, and the sharpest inversion in the series.
//
// ρ² = 1/cos 2θ is EXACTLY x² − y² = 1 a rectangular HYPERBOLA
// ρ² = cos 2θ is EXACTLY (x² + y²)² = x² − y² the LEMNISCATE OF BERNOULLI
//
// checked to 1e-15 and 4e-16. One reciprocal apart, so THE LEMNISCATE IS THE INVERSE OF THE
// HYPERBOLA — the cleanest statement this whole herbarium has to make, and the reason
// Bernoulli's curve keeps turning up wherever a hyperbola does.
//
// The other two columns are inversions as well, and both name curves this series has met:
//
// ρ = cos 3θ the regular TRIFOLIUM, three petals, (x²+y²)² = x(x² − 3y²)
// ρ = 1/cos 3θ the EQUILATERAL TREFOIL — and it is exactly y² = x²(x−1)/(3x+1), which
// examples/zoo-exponents-4.manic drew from the Cartesian table. Checked: 4e-15.
// Its ±30° oblique asymptotes, the reason for "equilateral", are the three
// directions cos 3θ vanishes in.
//
// ρ = cos(θ/2) the FOLIUM OF DÜRER, which needs θ over 4π to close because the half
// angle halves the frequency
// ρ = 1/cos(θ/2) the TRISECTRIX OF DELANGE
//
// THE CRUCIFORM IS THE SAME CURVE TWICE. ρ = 1/cos 2θ gives (x² − y²)² = x² + y². Turn that
// through 45° and it becomes 4X²Y² = X² + Y² — which is examples/zoo-exponents-3.manic's
// cruciform x²y² = x² + y², scaled. Verified to 7e-14. The polar table and the Cartesian one
// disagreed about which way up to draw it, and nothing else.
//
// The remaining three are what the multiple angle buys. ρ² = 1/cos 4θ is the MALTESE CROSS,
// x⁴ − 6x²y² + y⁴ = x² + y², to 1e-14: doubling the angle again doubles the arms. ρ² = cos 3θ
// is KIEPERT'S CURVE. And ρ = cos²(θ/2) is the CARDIOID, because cos²(θ/2) is (1 + cos θ)/2
// exactly — the half angle in disguise, satisfying (2(x²+y²) − x)² = x² + y² to 1e-15.
//
// manic examples/zoo-polar-3.manic
title("The lemniscate is the inverse of the hyperbola");
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, "Multiple angles — and the sharpest inversion in the herbarium", (540, 116), 22);
coords(k_lem, (130.00, 410.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 95.0000, 95.0000, 1, 0.5, 0);
color(k_lem, dim); opacity(k_lem, 0.35);
coords(k_tri, (316.39, 410.00), (-0.6613, 1.0988), (-0.8801, 0.8801), 107.9442, 107.9442, 1, 0.5, 0);
color(k_tri, dim); opacity(k_tri, 0.35);
coords(k_dur, (550.00, 410.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 95.0000, 95.0000, 1, 0.5, 0);
color(k_dur, dim); opacity(k_dur, 0.35);
coords(k_cru, (760.00, 410.00), (-3.7162, 3.7162), (-3.7162, 3.7162), 25.5637, 25.5637, 1, 2.0, 0);
color(k_cru, dim); opacity(k_cru, 0.35);
coords(k_kie, (970.00, 410.00), (-1.0000, 1.0000), (-1.0000, 1.0000), 95.0000, 95.0000, 1, 0.5, 0);
color(k_kie, dim); opacity(k_kie, 0.35);
coords(k_hyp, (130.00, 910.00), (-1.8552, 1.8552), (-1.8552, 1.8552), 51.2076, 51.2076, 1, 1.0, 0);
color(k_hyp, dim); opacity(k_hyp, 0.35);
coords(k_trf, (336.97, 910.00), (-5.0949, 5.4300), (-5.2625, 5.2625), 18.0524, 18.0524, 1, 2.0, 0);
color(k_trf, dim); opacity(k_trf, 0.35);
coords(k_del, (566.42, 910.00), (-2.1411, 1.5100), (-1.8255, 1.8255), 52.0397, 52.0397, 1, 1.0, 0);
color(k_del, dim); opacity(k_del, 0.35);
coords(k_mal, (760.00, 910.00), (-1.9746, 1.9746), (-1.9746, 1.9746), 48.1112, 48.1112, 1, 1.0, 0);
color(k_mal, dim); opacity(k_mal, 0.35);
coords(k_car, (906.01, 910.00), (-0.2120, 1.0870), (-0.6495, 0.6495), 146.2621, 146.2621, 1, 0.25, 0);
color(k_car, dim); opacity(k_car, 0.35);
param(lem0, (130.00, 410.00), 95.0000, 95.0000,
"(sqrt(max(cos(2*t), 0)))*cos(t)", "(sqrt(max(cos(2*t), 0)))*sin(t)", (-0.78200, 0.78200));
color(lem0, gold); stroke(lem0, 4); untraced(lem0);
param(lem1, (130.00, 410.00), 95.0000, 95.0000,
"(0 - sqrt(max(cos(2*t), 0)))*cos(t)", "(0 - sqrt(max(cos(2*t), 0)))*sin(t)", (-0.78200, 0.78200));
color(lem1, gold); stroke(lem1, 4); untraced(lem1);
param(tri0, (316.39, 410.00), 107.9442, 107.9442,
"(cos(3*t))*cos(t)", "(cos(3*t))*sin(t)", (0.00000, 3.14159));
color(tri0, mint); stroke(tri0, 4); untraced(tri0);
param(dur0, (550.00, 410.00), 95.0000, 95.0000,
"(cos(t/2))*cos(t)", "(cos(t/2))*sin(t)", (0.00000, 12.56637));
color(dur0, cyan); stroke(dur0, 4); untraced(dur0);
param(cru0, (760.00, 410.00), 25.5637, 25.5637,
"(1/cos(2*t))*cos(t)", "(1/cos(2*t))*sin(t)", (-0.68000, 0.68000));
color(cru0, violet); stroke(cru0, 4); untraced(cru0);
param(cru1, (760.00, 410.00), 25.5637, 25.5637,
"(1/cos(2*t))*cos(t)", "(1/cos(2*t))*sin(t)", (0.89080, 2.25080));
color(cru1, violet); stroke(cru1, 4); untraced(cru1);
param(cru2, (760.00, 410.00), 25.5637, 25.5637,
"(1/cos(2*t))*cos(t)", "(1/cos(2*t))*sin(t)", (2.46159, 3.82159));
color(cru2, violet); stroke(cru2, 4); untraced(cru2);
param(cru3, (760.00, 410.00), 25.5637, 25.5637,
"(1/cos(2*t))*cos(t)", "(1/cos(2*t))*sin(t)", (4.03239, 5.39239));
color(cru3, violet); stroke(cru3, 4); untraced(cru3);
param(kie0, (970.00, 410.00), 95.0000, 95.0000,
"(sqrt(max(cos(3*t), 0)))*cos(t)", "(sqrt(max(cos(3*t), 0)))*sin(t)", (-0.52000, 0.52000));
color(kie0, coral); stroke(kie0, 4); untraced(kie0);
param(kie1, (970.00, 410.00), 95.0000, 95.0000,
"(sqrt(max(cos(3*t), 0)))*cos(t)", "(sqrt(max(cos(3*t), 0)))*sin(t)", (1.57440, 2.61440));
color(kie1, coral); stroke(kie1, 4); untraced(kie1);
param(kie2, (970.00, 410.00), 95.0000, 95.0000,
"(sqrt(max(cos(3*t), 0)))*cos(t)", "(sqrt(max(cos(3*t), 0)))*sin(t)", (3.66879, 4.70879));
color(kie2, coral); stroke(kie2, 4); untraced(kie2);
param(kie3, (970.00, 410.00), 95.0000, 95.0000,
"(0 - sqrt(max(cos(3*t), 0)))*cos(t)", "(0 - sqrt(max(cos(3*t), 0)))*sin(t)", (-0.52000, 0.52000));
color(kie3, coral); stroke(kie3, 4); untraced(kie3);
param(kie4, (970.00, 410.00), 95.0000, 95.0000,
"(0 - sqrt(max(cos(3*t), 0)))*cos(t)", "(0 - sqrt(max(cos(3*t), 0)))*sin(t)", (1.57440, 2.61440));
color(kie4, coral); stroke(kie4, 4); untraced(kie4);
param(kie5, (970.00, 410.00), 95.0000, 95.0000,
"(0 - sqrt(max(cos(3*t), 0)))*cos(t)", "(0 - sqrt(max(cos(3*t), 0)))*sin(t)", (3.66879, 4.70879));
color(kie5, coral); stroke(kie5, 4); untraced(kie5);
param(hyp0, (130.00, 910.00), 51.2076, 51.2076,
"(1/sqrt(max(cos(2*t), 0.000001)))*cos(t)", "(1/sqrt(max(cos(2*t), 0.000001)))*sin(t)", (-0.70000, 0.70000));
color(hyp0, gold); stroke(hyp0, 4); untraced(hyp0);
param(hyp1, (130.00, 910.00), 51.2076, 51.2076,
"(0 - 1/sqrt(max(cos(2*t), 0.000001)))*cos(t)", "(0 - 1/sqrt(max(cos(2*t), 0.000001)))*sin(t)", (-0.70000, 0.70000));
color(hyp1, gold); stroke(hyp1, 4); untraced(hyp1);
param(trf0, (336.97, 910.00), 18.0524, 18.0524,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (-0.46000, 0.46000));
color(trf0, mint); stroke(trf0, 4); untraced(trf0);
param(trf1, (336.97, 910.00), 18.0524, 18.0524,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (1.63440, 2.55440));
color(trf1, mint); stroke(trf1, 4); untraced(trf1);
param(trf2, (336.97, 910.00), 18.0524, 18.0524,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (3.72879, 4.64879));
color(trf2, mint); stroke(trf2, 4); untraced(trf2);
param(del0, (566.42, 910.00), 52.0397, 52.0397,
"(1/cos(t/2))*cos(t)", "(1/cos(t/2))*sin(t)", (-2.30000, 2.30000));
color(del0, cyan); stroke(del0, 4); untraced(del0);
param(mal0, (760.00, 910.00), 48.1112, 48.1112,
"(1/sqrt(max(cos(4*t), 0.000001)))*cos(t)", "(1/sqrt(max(cos(4*t), 0.000001)))*sin(t)", (-0.33500, 0.33500));
color(mal0, violet); stroke(mal0, 4); untraced(mal0);
param(mal1, (760.00, 910.00), 48.1112, 48.1112,
"(1/sqrt(max(cos(4*t), 0.000001)))*cos(t)", "(1/sqrt(max(cos(4*t), 0.000001)))*sin(t)", (1.23580, 1.90580));
color(mal1, violet); stroke(mal1, 4); untraced(mal1);
param(mal2, (760.00, 910.00), 48.1112, 48.1112,
"(1/sqrt(max(cos(4*t), 0.000001)))*cos(t)", "(1/sqrt(max(cos(4*t), 0.000001)))*sin(t)", (2.80659, 3.47659));
color(mal2, violet); stroke(mal2, 4); untraced(mal2);
param(mal3, (760.00, 910.00), 48.1112, 48.1112,
"(1/sqrt(max(cos(4*t), 0.000001)))*cos(t)", "(1/sqrt(max(cos(4*t), 0.000001)))*sin(t)", (4.37739, 5.04739));
color(mal3, violet); stroke(mal3, 4); untraced(mal3);
param(car0, (906.01, 910.00), 146.2621, 146.2621,
"(cos(t/2)*cos(t/2))*cos(t)", "(cos(t/2)*cos(t/2))*sin(t)", (0.00000, 6.28318));
color(car0, coral); stroke(car0, 4); untraced(car0);
text(n_lem, (130, 535), "lemniscate of Bernoulli"); size(n_lem, 14); color(n_lem, fg);
equation(q_lem, (130, 579), `\rho^2=\cos 2\theta`, 22);
text(n_tri, (340, 535), "regular trifolium"); size(n_tri, 14); color(n_tri, fg);
equation(q_tri, (340, 579), `\rho=\cos 3\theta`, 22);
text(n_dur, (550, 535), "folium of Dürer"); size(n_dur, 14); color(n_dur, fg);
equation(q_dur, (550, 579), `\rho=\cos\frac{\theta}{2}`, 22);
text(n_cru, (760, 535), "cruciform"); size(n_cru, 14); color(n_cru, fg);
equation(q_cru, (760, 579), `\rho=\frac{1}{\cos 2\theta}`, 22);
text(n_kie, (970, 535), "curve of Kiepert"); size(n_kie, 14); color(n_kie, fg);
equation(q_kie, (970, 579), `\rho^2=\cos 3\theta`, 22);
text(n_hyp, (130, 1035), "rectangular hyperbola"); size(n_hyp, 14); color(n_hyp, fg);
equation(q_hyp, (130, 1079), `\rho^2=\frac{1}{\cos 2\theta}`, 22);
text(n_trf, (340, 1035), "equilateral trefoil"); size(n_trf, 14); color(n_trf, fg);
equation(q_trf, (340, 1079), `\rho=\frac{1}{\cos 3\theta}`, 22);
text(n_del, (550, 1035), "trisectrix of Delange"); size(n_del, 14); color(n_del, fg);
equation(q_del, (550, 1079), `\rho=\frac{1}{\cos\frac{\theta}{2}}`, 22);
text(n_mal, (760, 1035), "Maltese cross"); size(n_mal, 14); color(n_mal, fg);
equation(q_mal, (760, 1079), `\rho^2=\frac{1}{\cos 4\theta}`, 22);
text(n_car, (970, 1035), "cardioid"); size(n_car, 14); color(n_car, fg);
equation(q_car, (970, 1079), `\rho=\cos^2\frac{\theta}{2}`, 22);
text(r_130, (130, 690), "inverses"); size(r_130, 16); color(r_130, coral);
text(r_340, (340, 690), "inverses"); size(r_340, 16); color(r_340, coral);
text(r_550, (550, 690), "inverses"); size(r_550, 16); color(r_550, coral);
text(n1, (540, 1180),
"ρ² = 1/cos 2θ is x² − y² = 1. ρ² = cos 2θ is (x²+y²)² = x² − y². The lemniscate is the hyperbola inverted.");
size(n1, 19); color(n1, fg); wrap(n1, 990);
text(n2, (540, 1290),
"ρ = 1/cos 3θ is the equilateral trefoil the Cartesian table drew as y² = x²(x−1)/(3x+1) — same curve, other spelling.");
size(n2, 18); color(n2, dim); wrap(n2, 990);
equation(eq, (540, 1450), `\rho^2=\cos 2\theta\;\longleftrightarrow\;\rho^2=\frac{1}{\cos 2\theta}`, 38);
par {
draw(lem0, 3.0);
draw(lem1, 3.0);
seq { wait(1.4); draw(tri0, 3.0); }
seq { wait(2.8); draw(dur0, 3.0); }
seq { wait(4.2); draw(cru0, 3.0); }
seq { wait(4.2); draw(cru1, 3.0); }
seq { wait(4.2); draw(cru2, 3.0); }
seq { wait(4.2); draw(cru3, 3.0); }
seq { wait(5.6); draw(kie0, 3.0); }
seq { wait(5.6); draw(kie1, 3.0); }
seq { wait(5.6); draw(kie2, 3.0); }
seq { wait(5.6); draw(kie3, 3.0); }
seq { wait(5.6); draw(kie4, 3.0); }
seq { wait(5.6); draw(kie5, 3.0); }
seq { wait(7.0); draw(hyp0, 3.0); }
seq { wait(7.0); draw(hyp1, 3.0); }
seq { wait(8.4); draw(trf0, 3.0); }
seq { wait(8.4); draw(trf1, 3.0); }
seq { wait(8.4); draw(trf2, 3.0); }
seq { wait(9.8); draw(del0, 3.0); }
seq { wait(11.2); draw(mal0, 3.0); }
seq { wait(11.2); draw(mal1, 3.0); }
seq { wait(11.2); draw(mal2, 3.0); }
seq { wait(11.2); draw(mal3, 3.0); }
seq { wait(12.6); draw(car0, 3.0); }
}
wait(3.4);
zoo-polar-4
Three more inverse pairs, and the discovery that the two tables do not agree about where to put the origin. Each row starts with ρ = f(θ) and ρ = 1/f(θ): the TRISECTOR LIMAÇON ↔ the TRISECTRIX OF MACLAURIN, CAYLEY’S SEXTIC ↔ TSCHIRNHAUSEN’S CUBIC (the classical statement), and the RIGHT STROPHOID ↔ a HYPERBOLA x² − y² = x — which pairs with zoo-polar-3, where the lemniscate turned out to be an inverted hyperbola too. AND A PAIR SPANNING TWO SCENES: ρ = cos²(θ/2) is zoo-polar-3’s cardioid and ρ = 1/cos²(θ/2) is here, exactly y² = 4(1−x), a parabola (1e-14) — so the cardioid is the inverse of a parabola about its focus, the two halves sitting in different scenes because the table put them there. THE ORIGIN IS NOT AGREED: ρ = 1/cos(θ/3) looks like a new curve until you eliminate θ, and then it is y² = (x+2)²(1−x)/(x+3) — which is y² = u²(3−u)/(u+1) with u = x+2, the trisectrix of Maclaurin zoo-exponents-4 drew, moved two to the left. 4e-13 in the shifted frame, 5e+05 before shifting. The polar form puts the pole at the FOCUS, the Cartesian at the NODE. zoo-polar-3’s cruciform needed a 45° turn for the same reason: two of this series’ apparent contradictions were a choice of coordinates. The right strophoid needs no shift — ρ = cos2θ/cosθ is exactly y² = x²(1−x)/(1+x) to 3e-13.
// zoo-polar-4 — three more inverse pairs, and the discovery that the two tables do not agree
// about where to put the origin.
//
// EACH ROW IS A PAIR. The left two cells of every row are ρ = f(θ) and ρ = 1/f(θ):
//
// ρ = cos(θ/3) the TRISECTOR LIMAÇON ↔ ρ = 1/cos(θ/3) the TRISECTRIX OF MACLAURIN
// ρ = cos³(θ/3) CAYLEY'S SEXTIC ↔ ρ = 1/cos³(θ/3) TSCHIRNHAUSEN'S CUBIC
// ρ = cos 2θ/cos θ the RIGHT STROPHOID ↔ ρ = cos θ/cos 2θ a HYPERBOLA, x² − y² = x
//
// The middle one is the classical statement that Cayley's sextic inverts to Tschirnhausen's
// cubic. The third says the right strophoid is an inverted hyperbola — which pairs with
// examples/zoo-polar-3.manic, where the lemniscate turned out to be one as well.
//
// AND A PAIR THAT SPANS TWO BATCHES. ρ = cos²(θ/2) is the cardioid of zoo-polar-3;
// ρ = 1/cos²(θ/2) is here, and it is exactly y² = 4(1 − x), a parabola, to 1e-14. So the
// cardioid is the inverse of a parabola about its focus — the two halves of that fact sitting
// in two different scenes because the table put them there.
//
// THE ORIGIN IS NOT AGREED BETWEEN THE TABLES, and this batch is where that shows. ρ = 1/cos(θ/3)
// looks like a new curve until you eliminate θ, and then it is
//
// y² = (x + 2)²(1 − x)/(x + 3)
//
// which is y² = u²(3 − u)/(u + 1) with u = x + 2 — the trisectrix of Maclaurin that
// examples/zoo-exponents-4.manic drew, moved two to the left. Residual 4e-13 in the shifted
// frame, and a residual of 5e+05 before I shifted it. The polar form puts the pole at the
// curve's FOCUS; the Cartesian form puts the origin at its NODE. Same curve, and the first
// check said otherwise only because the two tables disagree about where to stand.
//
// examples/zoo-polar-3.manic had the same thing happen with the cruciform, which needed a
// 45° turn rather than a shift. Two of the series' apparent contradictions have now turned
// out to be a choice of coordinates.
//
// The right strophoid checks out directly, no shift needed: ρ = cos 2θ/cos θ is exactly
// y² = x²(1 − x)/(1 + x), which zoo-exponents-4 drew, to 3e-13.
//
// The last column is what is left: the bow-tie, and the parabolic folium — cos 2θ over a
// rising power of cos θ, one after the other.
//
// manic examples/zoo-polar-4.manic
title("Three more inversions, and where the two tables disagree");
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, "Each row starts with a curve and its inverse", (540, 116), 24);
coords(k_lim, (200.17, 350.00), (-0.6613, 1.0988), (-0.8801, 0.8801), 136.3506, 136.3506, 1, 0.5, 0);
color(k_lim, dim); opacity(k_lim, 0.35);
coords(k_mac, (562.05, 350.00), (-6.0701, 4.1857), (-5.1279, 5.1279), 23.4014, 23.4014, 1, 2.0, 0);
color(k_mac, dim); opacity(k_mac, 0.35);
coords(k_pab, (890.18, 350.00), (-5.9655, 2.9725), (-4.4690, 4.4690), 26.8517, 26.8517, 1, 2.0, 0);
color(k_pab, dim); opacity(k_pab, 0.35);
coords(k_cay, (168.23, 780.00), (-0.3536, 1.1036), (-0.7286, 0.7286), 164.7100, 164.7100, 1, 0.5, 0);
color(k_cay, dim); opacity(k_cay, 0.35);
coords(k_tsc, (618.41, 780.00), (-4.7711, 1.0000), (-2.8856, 2.8856), 41.5864, 41.5864, 1, 1.0, 0);
color(k_tsc, dim); opacity(k_tsc, 0.35);
coords(k_bow, (850.00, 780.00), (-20.3797, 20.3797), (-20.3797, 20.3797), 5.8882, 5.8882, 1, 10.0, 0);
color(k_bow, dim); opacity(k_bow, 0.35);
coords(k_str, (229.57, 1210.00), (-6.2615, 6.3066), (-6.2841, 6.2841), 19.0959, 19.0959, 1, 5.0, 0);
color(k_str, dim); opacity(k_str, 0.35);
coords(k_hyp, (521.97, 1210.00), (-2.8283, 3.8283), (-3.3283, 3.3283), 36.0541, 36.0541, 1, 2.0, 0);
color(k_hyp, dim); opacity(k_hyp, 0.35);
coords(k_fol, (867.85, 1210.00), (-25.0190, 18.5396), (-21.7793, 21.7793), 5.5098, 5.5098, 1, 10.0, 0);
color(k_fol, dim); opacity(k_fol, 0.35);
param(lim0, (200.17, 350.00), 136.3506, 136.3506,
"(cos(t/3))*cos(t)", "(cos(t/3))*sin(t)", (-4.71239, 4.71239));
color(lim0, gold); stroke(lim0, 4); untraced(lim0);
param(mac0, (562.05, 350.00), 23.4014, 23.4014,
"(1/cos(t/3))*cos(t)", "(1/cos(t/3))*sin(t)", (-4.20000, 4.20000));
color(mac0, gold); stroke(mac0, 4); untraced(mac0);
param(pab0, (890.18, 350.00), 26.8517, 26.8517,
"(1/(cos(t/2)*cos(t/2)))*cos(t)", "(1/(cos(t/2)*cos(t/2)))*sin(t)", (-2.30000, 2.30000));
color(pab0, mint); stroke(pab0, 4); untraced(pab0);
param(cay0, (168.23, 780.00), 164.7100, 164.7100,
"(cos(t/3)*cos(t/3)*cos(t/3))*cos(t)", "(cos(t/3)*cos(t/3)*cos(t/3))*sin(t)", (-4.71239, 4.71239));
color(cay0, cyan); stroke(cay0, 4); untraced(cay0);
param(tsc0, (618.41, 780.00), 41.5864, 41.5864,
"(1/(cos(t/3)*cos(t/3)*cos(t/3)))*cos(t)", "(1/(cos(t/3)*cos(t/3)*cos(t/3)))*sin(t)", (-3.06, 3.06));
color(tsc0, cyan); stroke(tsc0, 4); untraced(tsc0);
param(bow0, (850.00, 780.00), 5.8882, 5.8882,
"(cos(2*t)/(cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)))*sin(t)", (-1.36000, 1.36000));
color(bow0, violet); stroke(bow0, 4); untraced(bow0);
param(bow1, (850.00, 780.00), 5.8882, 5.8882,
"(cos(2*t)/(cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)))*sin(t)", (1.78159, 4.50159));
color(bow1, violet); stroke(bow1, 4); untraced(bow1);
param(str0, (229.57, 1210.00), 19.0959, 19.0959,
"(cos(2*t)/cos(t))*cos(t)", "(cos(2*t)/cos(t))*sin(t)", (-1.42000, 1.42000));
color(str0, coral); stroke(str0, 4); untraced(str0);
param(str1, (229.57, 1210.00), 19.0959, 19.0959,
"(cos(2*t)/cos(t))*cos(t)", "(cos(2*t)/cos(t))*sin(t)", (1.72159, 4.56159));
color(str1, coral); stroke(str1, 4); untraced(str1);
param(hyp0, (521.97, 1210.00), 36.0541, 36.0541,
"(cos(t)/cos(2*t))*cos(t)", "(cos(t)/cos(2*t))*sin(t)", (-0.71000, 0.71000));
color(hyp0, coral); stroke(hyp0, 4); untraced(hyp0);
param(hyp1, (521.97, 1210.00), 36.0541, 36.0541,
"(cos(t)/cos(2*t))*cos(t)", "(cos(t)/cos(2*t))*sin(t)", (0.86080, 2.28080));
color(hyp1, coral); stroke(hyp1, 4); untraced(hyp1);
param(hyp2, (521.97, 1210.00), 36.0541, 36.0541,
"(cos(t)/cos(2*t))*cos(t)", "(cos(t)/cos(2*t))*sin(t)", (2.43159, 3.85159));
color(hyp2, coral); stroke(hyp2, 4); untraced(hyp2);
param(hyp3, (521.97, 1210.00), 36.0541, 36.0541,
"(cos(t)/cos(2*t))*cos(t)", "(cos(t)/cos(2*t))*sin(t)", (4.00239, 5.42239));
color(hyp3, coral); stroke(hyp3, 4); untraced(hyp3);
param(fol0, (867.85, 1210.00), 5.5098, 5.5098,
"(cos(2*t)/(cos(t)*cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)*cos(t)))*sin(t)", (-1.24000, 1.24000));
color(fol0, violet); stroke(fol0, 4); untraced(fol0);
param(fol1, (867.85, 1210.00), 5.5098, 5.5098,
"(cos(2*t)/(cos(t)*cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)*cos(t)))*sin(t)", (1.90159, 4.38159));
color(fol1, violet); stroke(fol1, 4); untraced(fol1);
text(n_lim, (230, 504), "trisector limaçon"); size(n_lim, 18); color(n_lim, fg);
equation(q_lim, (230, 552), `\rho=\cos\frac{\theta}{3}`, 25);
text(n_mac, (540, 504), "trisectrix of Maclaurin"); size(n_mac, 18); color(n_mac, fg);
equation(q_mac, (540, 552), `\rho=\frac{1}{\cos\frac{\theta}{3}}`, 25);
text(n_pab, (850, 504), "parabola"); size(n_pab, 18); color(n_pab, fg);
equation(q_pab, (850, 552), `\rho=\frac{1}{\cos^2\frac{\theta}{2}}`, 25);
text(n_cay, (230, 934), "Cayley's sextic"); size(n_cay, 18); color(n_cay, fg);
equation(q_cay, (230, 982), `\rho=\cos^3\frac{\theta}{3}`, 25);
text(n_tsc, (540, 934), "Tschirnhausen cubic"); size(n_tsc, 18); color(n_tsc, fg);
equation(q_tsc, (540, 982), `\rho=\frac{1}{\cos^3\frac{\theta}{3}}`, 25);
text(n_bow, (850, 934), "bow-tie"); size(n_bow, 18); color(n_bow, fg);
equation(q_bow, (850, 982), `\rho=\frac{\cos 2\theta}{\cos^2\theta}`, 25);
text(n_str, (230, 1364), "right strophoid"); size(n_str, 18); color(n_str, fg);
equation(q_str, (230, 1412), `\rho=\frac{\cos 2\theta}{\cos\theta}`, 25);
text(n_hyp, (540, 1364), "hyperbola"); size(n_hyp, 18); color(n_hyp, fg);
equation(q_hyp, (540, 1412), `\rho=\frac{\cos\theta}{\cos 2\theta}`, 25);
text(n_fol, (850, 1364), "parabolic folium"); size(n_fol, 18); color(n_fol, fg);
equation(q_fol, (850, 1412), `\rho=\frac{\cos 2\theta}{\cos^3\theta}`, 25);
text(p_350, (385, 206), "inverses"); size(p_350, 16); color(p_350, coral);
text(p_780, (385, 636), "inverses"); size(p_780, 16); color(p_780, coral);
text(p_1210, (385, 1066), "inverses"); size(p_1210, 16); color(p_1210, coral);
text(n1, (540, 1520),
"Cayley's sextic inverts to Tschirnhausen's cubic; the right strophoid inverts to a hyperbola; and the cardioid of the last scene inverts to the parabola in this one.");
size(n1, 19); color(n1, fg); wrap(n1, 990);
text(n2, (540, 1630),
"ρ = 1/cos(θ/3) is the trisectrix of Maclaurin with the pole at its focus, not its node — the tables disagree about where to stand, not about the curve.");
size(n2, 18); color(n2, dim); wrap(n2, 990);
equation(eq, (540, 1780), `\rho=f(\theta)\;\longleftrightarrow\;\rho=\frac{1}{f(\theta)}`, 38);
par {
draw(lim0, 3.2);
seq { wait(1.6); draw(mac0, 3.2); }
seq { wait(3.2); draw(pab0, 3.2); }
seq { wait(4.8); draw(cay0, 3.2); }
seq { wait(6.4); draw(tsc0, 3.2); }
seq { wait(8.0); draw(bow0, 3.2); }
seq { wait(8.0); draw(bow1, 3.2); }
seq { wait(9.6); draw(str0, 3.2); }
seq { wait(9.6); draw(str1, 3.2); }
seq { wait(11.2); draw(hyp0, 3.2); }
seq { wait(11.2); draw(hyp1, 3.2); }
seq { wait(11.2); draw(hyp2, 3.2); }
seq { wait(11.2); draw(hyp3, 3.2); }
seq { wait(12.6); draw(fol0, 3.2); }
seq { wait(12.6); draw(fol1, 3.2); }
}
wait(3.4);
zoo-cartesian-complete
THE WHOLE CARTESIAN TABLE ON ONE PLATE, on paper, every cell in its own coordinate frame — 32 equations in the order the table gives them, coloured by family. TWENTY-SIX OF THE THIRTY-TWO ARE y² = R(x), AND THAT IS THE WHOLE PLATE: such a curve is symmetric about the x-axis, it EXISTS exactly where R ≥ 0, and every feature is read off the zeros and poles of R — a simple zero is a crossing, a pole is a vertical asymptote, and a DOUBLE zero is a singular point whose TYPE is decided by the sign of R nearby. That is the trichotomy the table opens with: y² = x³ (R changes sign) is a CUSP, y² = x²(x−1) (R < 0 both sides) an ACNODE, y² = x²(1−x) (R > 0 both sides) a CRUNODE — and it keeps paying, with huit a crunode and campyle an acnode for exactly the same reason, differing only in the sign of x²−1. AT A CRUNODE THE TANGENT SLOPES ARE ±√c where R = x²(c + …), and row seven is that one number: strophoid c = +1 gives ±45.00°, trisectrix of Maclaurin c = +3 gives ±60.00°, equilateral trefoil c = −1 and visiera c = −2 give no real slope at all and so are acnodes. c is exactly the ratio a/b at the origin, so the shape of the origin is settled before any of the curve is drawn. THE SIX ACNODES ARE MARKED WITH A DOT, because an isolated point is a real point of the curve that no plot can draw; scanning every y² = R(x) here for a zero of R negative on both sides finds exactly six, all at the origin, and each cell is framed to include its own. THE SIX THAT ARE NOT y² = R(x) are the exceptions worth knowing: y = x² and the two quotients y = 1/(1+x²), y = x/(1+x²) are single-valued and so not symmetric in y; y³ = 1−x³ is single-valued because the power is ODD; the bifolium needs FOUR square-root branches rather than two; and the bicorne’s ± sits in a denominator. 26 sign diagrams and 6 exceptions.
// zoo-cartesian-complete — the whole Cartesian table on one plate, on paper, every cell in
// its own frame. 32 equations, in the order the table gives them, coloured by family.
//
// TWENTY-SIX OF THE THIRTY-TWO ARE y2 = R(x), AND THAT IS THE WHOLE PLATE. For those the
// curve is symmetric about the x-axis, it EXISTS exactly where R >= 0, and every feature is
// read off the zeros and poles of R:
//
// simple zero of R the curve crosses the axis
// pole of R a vertical asymptote
// DOUBLE zero at x0 a singular point, and WHICH one is decided by the sign of R nearby
//
// That last line is the trichotomy, and it is the same three curves the table opens with:
//
// y2 = x^3 R changes sign CUSP parabole semi-cubique
// y2 = x^2(x-1) R < 0 both sides ACNODE cubique duplicatrice (an isolated point)
// y2 = x^2(1-x) R > 0 both sides CRUNODE cf. parabole divergente
//
// THE SIX ACNODES ARE MARKED WITH A DOT, because an isolated point is a real point of the
// curve that no plot can draw. Scanning every y2 = R(x) here for a zero of R with R < 0 on
// BOTH sides finds exactly six, all of them at the origin: cubique duplicatrice, campyle
// d'Eudoxe, cubique mixte, cruciforme, trefle equilatere and visiera. Each owns a point at
// the origin with nothing attached to it, and the cell is framed to include it.
//
// and it keeps paying: huit is a crunode and campyle an acnode for exactly the same reason,
// differing only in the sign of x^2 - 1; quartique piriforme is a cusp; double goutte d'eau
// a crunode.
//
// AT A CRUNODE THE TANGENT SLOPES ARE +/-sqrt(c), where R = x^2(c + ...). The a/b family in
// row seven is that one number:
//
// strophoide y2 = x^2(1-x)/(1+x) c = +1 slopes +/-45.00 deg
// trisectrice de Maclaurin y2 = x^2(3-x)/(1+x) c = +3 slopes +/-60.00 deg
// trefle equilatere y2 = x^2(x-1)/(3x+1) c = -1 no real slope -> ACNODE
// visiera y2 = x^2(2-x)/(x-1) c = -2 no real slope -> ACNODE
//
// c is exactly the ratio a/b at the origin, so the shape of the origin is decided before any
// of the curve is drawn. Checked: c = 0.999998 and 2.999996 against 1 and 3 at h = 1e-6.
//
// THE SIX THAT ARE NOT y2 = R(x) are the exceptions worth knowing: y = x^2 and the two
// bell-shaped quotients y = 1/(1+x2) and y = x/(1+x2), which are single-valued and so are
// not symmetric in y; y3 = 1 - x3, which is single-valued because the power is ODD; the
// bifolium, which needs FOUR square-root branches rather than two; and the bicorne, whose
// +/- sits in a denominator. So the plate is 26 sign diagrams and 6 exceptions.
//
// Nothing here is hand-placed. Each cell picks its own y-clip by matching the bounding box
// to the cell's aspect, cuts what survives into runs, and scales that to the frame.
//
// manic examples/zoo-cartesian-complete.manic
title("The whole Cartesian table, on one plate");
canvas("9:16");
template("paper");
text(brand, (540, 40), "maniclang.com");
display(brand); size(brand, 19); color(brand, ink); opacity(brand, 0.55);
caption(head, "32 equations, 26 sign diagrams", (540, 112), 25);
text(sub, (540, 156), "y2 = R(x): it exists where R >= 0, and R's double zeros decide the singular points");
size(sub, 15); color(sub, ink); opacity(sub, 0.6);
coords(k_par, (145.00, 362.00), (-1.1563, 1.1563), (0.0000, 1.3370), 89.7564, 89.7564, 0, 0.25, 0);
color(k_par, slate); opacity(k_par, 0.38);
plot(par0, (145.00, 362.00), 89.7564, 89.7564, "x*x", (-1.15627, 1.15627));
color(par0, #1f3f6e); stroke(par0, 3); untraced(par0);
equation(q_par, (145.0, 380.0), `y=x^2`, 16);
color(q_par, #1f3f6e); hidden(q_par);
text(n_par, (145.0, 402.0), "parabole");
size(n_par, 11); color(n_par, ink); opacity(n_par, 0.8); hidden(n_par);
coords(k_hyp, (409.00, 302.00), (-3.1680, 3.1680), (-3.3221, 3.3221), 18.0610, 18.0610, 0, 1.0, 0);
color(k_hyp, slate); opacity(k_hyp, 0.38);
plot(hyp0, (409.00, 302.00), 18.0610, 18.0610, "sqrt(abs(x*x + 1))", (-3.16800, 3.16800));
color(hyp0, #1f3f6e); stroke(hyp0, 3); untraced(hyp0);
plot(hyp1, (409.00, 302.00), 18.0610, 18.0610, "0 - sqrt(abs(x*x + 1))", (-3.16800, 3.16800));
color(hyp1, #1f3f6e); stroke(hyp1, 3); untraced(hyp1);
equation(q_hyp, (409.0, 380.0), `y^2=x^2+1`, 16);
color(q_hyp, #1f3f6e); hidden(q_hyp);
text(n_hyp, (409.0, 402.0), "hyperbole");
size(n_hyp, 11); color(n_hyp, ink); opacity(n_hyp, 0.8); hidden(n_hyp);
coords(k_sem, (656.15, 302.00), (0.0000, 3.1680), (-5.6387, 5.6387), 10.6408, 10.6408, 0, 2.0, 0);
color(k_sem, slate); opacity(k_sem, 0.38);
plot(sem0, (656.15, 302.00), 10.6408, 10.6408, "sqrt(abs(x*x*x))", (0.00000, 3.16800));
color(sem0, #1f3f6e); stroke(sem0, 3); untraced(sem0);
plot(sem1, (656.15, 302.00), 10.6408, 10.6408, "0 - sqrt(abs(x*x*x))", (0.00000, 3.16800));
color(sem1, #1f3f6e); stroke(sem1, 3); untraced(sem1);
equation(q_sem, (673.0, 380.0), `y^2=x^3`, 16);
color(q_sem, #1f3f6e); hidden(q_sem);
text(n_sem, (673.0, 402.0), "parabole semi-cubique");
size(n_sem, 11); color(n_sem, ink); opacity(n_sem, 0.8); hidden(n_sem);
coords(k_dup, (913.32, 302.00), (0.0000, 2.6048), (-3.2998, 3.2998), 18.1830, 18.1830, 0, 1.0, 0);
color(k_dup, slate); opacity(k_dup, 0.38);
plot(dup0, (913.32, 302.00), 18.1830, 18.1830, "sqrt(abs(x*x*(x - 1)))", (1.00053, 2.60480));
color(dup0, #1f3f6e); stroke(dup0, 3); untraced(dup0);
plot(dup1, (913.32, 302.00), 18.1830, 18.1830, "0 - sqrt(abs(x*x*(x - 1)))", (1.00053, 2.60480));
color(dup1, #1f3f6e); stroke(dup1, 3); untraced(dup1);
circle(ac_dup0, (913.32, 302.00), 4.5); color(ac_dup0, #1f3f6e); hidden(ac_dup0);
equation(q_dup, (937.0, 380.0), `y^2=x^2(x-1)`, 16);
color(q_dup, #1f3f6e); hidden(q_dup);
text(n_dup, (937.0, 402.0), "cubique duplicatrice");
size(n_dup, 11); color(n_dup, ink); opacity(n_dup, 0.8); hidden(n_dup);
coords(k_div, (92.75, 494.00), (-0.3307, 0.9984), (-0.3832, 0.3832), 156.5008, 156.5008, 0, 0.25, 0);
color(k_div, slate); opacity(k_div, 0.38);
plot(div0, (92.75, 494.00), 156.5008, 156.5008, "sqrt(abs(x*x*(1 - x)))", (-0.33067, 0.62933));
color(div0, #1f3f6e); stroke(div0, 3); untraced(div0);
plot(div1, (92.75, 494.00), 156.5008, 156.5008, "sqrt(abs(x*x*(1 - x)))", (0.70187, 0.99840));
color(div1, #1f3f6e); stroke(div1, 3); untraced(div1);
plot(div2, (92.75, 494.00), 156.5008, 156.5008, "0 - sqrt(abs(x*x*(1 - x)))", (-0.33067, 0.62933));
color(div2, #1f3f6e); stroke(div2, 3); untraced(div2);
plot(div3, (92.75, 494.00), 156.5008, 156.5008, "0 - sqrt(abs(x*x*(1 - x)))", (0.70187, 0.99840));
color(div3, #1f3f6e); stroke(div3, 3); untraced(div3);
equation(q_div, (145.0, 572.0), `y^2=x^2(1-x)`, 16);
color(q_div, #1f3f6e); hidden(q_div);
text(n_div, (145.0, 594.0), "cf. parabole divergente");
size(n_div, 11); color(n_div, ink); opacity(n_div, 0.8); hidden(n_div);
coords(k_lam, (408.39, 494.02), (-3.1360, 3.2000), (-3.1671, 3.1695), 18.9375, 18.9375, 0, 1.0, 0);
color(k_lam, slate); opacity(k_lam, 0.38);
plot(lam0, (408.39, 494.02), 18.9375, 18.9375, "sign(1 - x*x*x)*exp(0.3333333333*ln(abs(1 - x*x*x) + 0.000000001))", (-3.13600, 3.20000));
color(lam0, #9c4a1e); stroke(lam0, 3); untraced(lam0);
equation(q_lam, (409.0, 572.0), `y^3=1-x^3`, 16);
color(q_lam, #9c4a1e); hidden(q_lam);
text(n_lam, (409.0, 594.0), "cubique de Lamé");
size(n_lam, 11); color(n_lam, ink); opacity(n_lam, 0.8); hidden(n_lam);
coords(k_dip, (673.00, 494.00), (-0.9996, 0.9996), (-0.5769, 0.5769), 103.9986, 103.9986, 0, 0.25, 0);
color(k_dip, slate); opacity(k_dip, 0.38);
plot(dip0, (673.00, 494.00), 103.9986, 103.9986, "sqrt(abs(exp(0.6666666667*ln(abs(x) + 0.000000001)) - x*x))", (-0.99960, -0.64120));
color(dip0, #9c4a1e); stroke(dip0, 3); untraced(dip0);
plot(dip1, (673.00, 494.00), 103.9986, 103.9986, "sqrt(abs(exp(0.6666666667*ln(abs(x) + 0.000000001)) - x*x))", (-0.24733, 0.24733));
color(dip1, #9c4a1e); stroke(dip1, 3); untraced(dip1);
plot(dip2, (673.00, 494.00), 103.9986, 103.9986, "sqrt(abs(exp(0.6666666667*ln(abs(x) + 0.000000001)) - x*x))", (0.64120, 0.99960));
color(dip2, #9c4a1e); stroke(dip2, 3); untraced(dip2);
plot(dip3, (673.00, 494.00), 103.9986, 103.9986, "0 - sqrt(abs(exp(0.6666666667*ln(abs(x) + 0.000000001)) - x*x))", (-0.99960, -0.64120));
color(dip3, #9c4a1e); stroke(dip3, 3); untraced(dip3);
plot(dip4, (673.00, 494.00), 103.9986, 103.9986, "0 - sqrt(abs(exp(0.6666666667*ln(abs(x) + 0.000000001)) - x*x))", (-0.24733, 0.24733));
color(dip4, #9c4a1e); stroke(dip4, 3); untraced(dip4);
plot(dip5, (673.00, 494.00), 103.9986, 103.9986, "0 - sqrt(abs(exp(0.6666666667*ln(abs(x) + 0.000000001)) - x*x))", (0.64120, 0.99960));
color(dip5, #9c4a1e); stroke(dip5, 3); untraced(dip5);
equation(q_dip, (673.0, 572.0), `y^2=x^{2/3}-x^2`, 16);
color(q_dip, #9c4a1e); hidden(q_dip);
text(n_dip, (673.0, 594.0), "courbe du dipôle");
size(n_dip, 11); color(n_dip, ink); opacity(n_dip, 0.8); hidden(n_dip);
coords(k_oeu, (937.00, 494.00), (-0.9996, 0.9996), (-0.3849, 0.3849), 104.0416, 104.0416, 0, 0.25, 0);
color(k_oeu, slate); opacity(k_oeu, 0.38);
plot(oeu0, (937.00, 494.00), 104.0416, 104.0416, "sqrt(abs(exp(1.3333333333*ln(abs(x) + 0.000000001)) - x*x))", (-0.99960, -0.54973));
color(oeu0, #9c4a1e); stroke(oeu0, 3); untraced(oeu0);
plot(oeu1, (937.00, 494.00), 104.0416, 104.0416, "sqrt(abs(exp(1.3333333333*ln(abs(x) + 0.000000001)) - x*x))", (-0.53947, 0.53853));
color(oeu1, #9c4a1e); stroke(oeu1, 3); untraced(oeu1);
plot(oeu2, (937.00, 494.00), 104.0416, 104.0416, "sqrt(abs(exp(1.3333333333*ln(abs(x) + 0.000000001)) - x*x))", (0.54973, 0.99960));
color(oeu2, #9c4a1e); stroke(oeu2, 3); untraced(oeu2);
plot(oeu3, (937.00, 494.00), 104.0416, 104.0416, "0 - sqrt(abs(exp(1.3333333333*ln(abs(x) + 0.000000001)) - x*x))", (-0.99960, -0.54973));
color(oeu3, #9c4a1e); stroke(oeu3, 3); untraced(oeu3);
plot(oeu4, (937.00, 494.00), 104.0416, 104.0416, "0 - sqrt(abs(exp(1.3333333333*ln(abs(x) + 0.000000001)) - x*x))", (-0.53947, 0.53853));
color(oeu4, #9c4a1e); stroke(oeu4, 3); untraced(oeu4);
plot(oeu5, (937.00, 494.00), 104.0416, 104.0416, "0 - sqrt(abs(exp(1.3333333333*ln(abs(x) + 0.000000001)) - x*x))", (0.54973, 0.99960));
color(oeu5, #9c4a1e); stroke(oeu5, 3); untraced(oeu5);
equation(q_oeu, (937.0, 572.0), `y^2=x^{4/3}-x^2`, 16);
color(q_oeu, #9c4a1e); hidden(q_oeu);
text(n_oeu, (937.0, 594.0), "œuf double");
size(n_oeu, 11); color(n_oeu, ink); opacity(n_oeu, 0.8); hidden(n_oeu);
coords(k_fos, (41.11, 686.00), (0.0000, 0.9996), (-0.2887, 0.2887), 207.8592, 207.8592, 0, 0.25, 0);
color(k_fos, slate); opacity(k_fos, 0.38);
plot(fos0, (41.11, 686.00), 207.8592, 207.8592, "sqrt(abs(exp(1.5*ln(abs(x) + 0.000000001)) - x*x))", (0.00000, 0.34347));
color(fos0, #9c4a1e); stroke(fos0, 3); untraced(fos0);
plot(fos1, (41.11, 686.00), 207.8592, 207.8592, "sqrt(abs(exp(1.5*ln(abs(x) + 0.000000001)) - x*x))", (0.76767, 0.99960));
color(fos1, #9c4a1e); stroke(fos1, 3); untraced(fos1);
plot(fos2, (41.11, 686.00), 207.8592, 207.8592, "0 - sqrt(abs(exp(1.5*ln(abs(x) + 0.000000001)) - x*x))", (0.00000, 0.34347));
color(fos2, #9c4a1e); stroke(fos2, 3); untraced(fos2);
plot(fos3, (41.11, 686.00), 207.8592, 207.8592, "0 - sqrt(abs(exp(1.5*ln(abs(x) + 0.000000001)) - x*x))", (0.76767, 0.99960));
color(fos3, #9c4a1e); stroke(fos3, 3); untraced(fos3);
equation(q_fos, (145.0, 764.0), `y^2=x^{3/2}-x^2`, 16);
color(q_fos, #9c4a1e); hidden(q_fos);
text(n_fos, (145.0, 786.0), "folium simple");
size(n_fos, 11); color(n_fos, ink); opacity(n_fos, 0.8); hidden(n_fos);
coords(k_hui, (409.00, 686.00), (-0.9996, 0.9996), (-0.4999, 0.4999), 104.0416, 104.0416, 0, 0.25, 0);
color(k_hui, slate); opacity(k_hui, 0.38);
plot(hui0, (409.00, 686.00), 104.0416, 104.0416, "sqrt(abs(x*x*(1 - x*x)))", (-0.99960, -0.71213));
color(hui0, #2f6446); stroke(hui0, 3); untraced(hui0);
plot(hui1, (409.00, 686.00), 104.0416, 104.0416, "sqrt(abs(x*x*(1 - x*x)))", (-0.70187, 0.70187));
color(hui1, #2f6446); stroke(hui1, 3); untraced(hui1);
plot(hui2, (409.00, 686.00), 104.0416, 104.0416, "sqrt(abs(x*x*(1 - x*x)))", (0.71213, 0.99960));
color(hui2, #2f6446); stroke(hui2, 3); untraced(hui2);
plot(hui3, (409.00, 686.00), 104.0416, 104.0416, "0 - sqrt(abs(x*x*(1 - x*x)))", (-0.99960, -0.71213));
color(hui3, #2f6446); stroke(hui3, 3); untraced(hui3);
plot(hui4, (409.00, 686.00), 104.0416, 104.0416, "0 - sqrt(abs(x*x*(1 - x*x)))", (-0.70187, 0.70187));
color(hui4, #2f6446); stroke(hui4, 3); untraced(hui4);
plot(hui5, (409.00, 686.00), 104.0416, 104.0416, "0 - sqrt(abs(x*x*(1 - x*x)))", (0.71213, 0.99960));
color(hui5, #2f6446); stroke(hui5, 3); untraced(hui5);
equation(q_hui, (409.0, 764.0), `y^2=x^2(1-x^2)`, 16);
color(q_hui, #2f6446); hidden(q_hui);
text(n_hui, (409.0, 786.0), "huit");
size(n_hui, 11); color(n_hui, ink); opacity(n_hui, 0.8); hidden(n_hui);
coords(k_pir, (569.37, 686.00), (0.0002, 0.9998), (-0.2895, 0.2895), 207.2624, 207.2624, 0, 0.25, 0);
color(k_pir, slate); opacity(k_pir, 0.38);
plot(pir0, (569.37, 686.00), 207.2624, 207.2624, "sqrt(abs(x*x*x*(1 - x)))", (0.00020, 0.58820));
color(pir0, #2f6446); stroke(pir0, 3); untraced(pir0);
plot(pir1, (569.37, 686.00), 207.2624, 207.2624, "sqrt(abs(x*x*x*(1 - x)))", (0.87500, 0.99980));
color(pir1, #2f6446); stroke(pir1, 3); untraced(pir1);
plot(pir2, (569.37, 686.00), 207.2624, 207.2624, "0 - sqrt(abs(x*x*x*(1 - x)))", (0.00020, 0.58820));
color(pir2, #2f6446); stroke(pir2, 3); untraced(pir2);
plot(pir3, (569.37, 686.00), 207.2624, 207.2624, "0 - sqrt(abs(x*x*x*(1 - x)))", (0.87500, 0.99980));
color(pir3, #2f6446); stroke(pir3, 3); untraced(pir3);
equation(q_pir, (673.0, 764.0), `y^2=x^3(1-x)`, 16);
color(q_pir, #2f6446); hidden(q_pir);
text(n_pir, (673.0, 786.0), "quartique piriforme");
size(n_pir, 11); color(n_pir, ink); opacity(n_pir, 0.8); hidden(n_pir);
coords(k_cam, (937.00, 686.00), (-1.6501, 1.6501), (-2.1660, 2.1660), 27.7011, 27.7011, 0, 0.5, 0);
color(k_cam, slate); opacity(k_cam, 0.38);
plot(cam0, (937.00, 686.00), 27.7011, 27.7011, "sqrt(abs(x*x*(x*x - 1)))", (-1.65013, -1.00013));
color(cam0, #2f6446); stroke(cam0, 3); untraced(cam0);
plot(cam1, (937.00, 686.00), 27.7011, 27.7011, "sqrt(abs(x*x*(x*x - 1)))", (1.00013, 1.65013));
color(cam1, #2f6446); stroke(cam1, 3); untraced(cam1);
plot(cam2, (937.00, 686.00), 27.7011, 27.7011, "0 - sqrt(abs(x*x*(x*x - 1)))", (-1.65013, -1.00013));
color(cam2, #2f6446); stroke(cam2, 3); untraced(cam2);
plot(cam3, (937.00, 686.00), 27.7011, 27.7011, "0 - sqrt(abs(x*x*(x*x - 1)))", (1.00013, 1.65013));
color(cam3, #2f6446); stroke(cam3, 3); untraced(cam3);
circle(ac_cam0, (937.00, 686.00), 4.5); color(ac_cam0, #2f6446); hidden(ac_cam0);
equation(q_cam, (937.0, 764.0), `y^2=x^2(x^2-1)`, 16);
color(q_cam, #2f6446); hidden(q_cam);
text(n_cam, (937.0, 786.0), "campyle d'Eudoxe");
size(n_cam, 11); color(n_cam, ink); opacity(n_cam, 0.8); hidden(n_cam);
coords(k_bou, (145.00, 878.00), (-0.9996, 0.9996), (-0.5867, 0.5867), 102.2588, 102.2588, 0, 0.25, 0);
color(k_bou, slate); opacity(k_bou, 0.38);
plot(bou0, (145.00, 878.00), 102.2588, 102.2588, "sqrt(abs((1 - x*x)*(1 - x*x)*(1 - x*x)))", (-0.99960, -0.54693));
color(bou0, #6b2f5e); stroke(bou0, 3); untraced(bou0);
plot(bou1, (145.00, 878.00), 102.2588, 102.2588, "sqrt(abs((1 - x*x)*(1 - x*x)*(1 - x*x)))", (0.54693, 0.99960));
color(bou1, #6b2f5e); stroke(bou1, 3); untraced(bou1);
plot(bou2, (145.00, 878.00), 102.2588, 102.2588, "0 - sqrt(abs((1 - x*x)*(1 - x*x)*(1 - x*x)))", (-0.99960, -0.54693));
color(bou2, #6b2f5e); stroke(bou2, 3); untraced(bou2);
plot(bou3, (145.00, 878.00), 102.2588, 102.2588, "0 - sqrt(abs((1 - x*x)*(1 - x*x)*(1 - x*x)))", (0.54693, 0.99960));
color(bou3, #6b2f5e); stroke(bou3, 3); untraced(bou3);
equation(q_bou, (145.0, 956.0), `y^2=(1-x^2)^3`, 16);
color(q_bou, #6b2f5e); hidden(q_bou);
text(n_bou, (145.0, 978.0), "bouche");
size(n_bou, 11); color(n_bou, ink); opacity(n_bou, 0.8); hidden(n_bou);
coords(k_ast, (409.00, 878.00), (-0.9996, 0.9996), (-0.5872, 0.5872), 102.1857, 102.1857, 0, 0.25, 0);
color(k_ast, slate); opacity(k_ast, 0.38);
plot(ast0, (409.00, 878.00), 102.1857, 102.1857, "sqrt(abs((1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))*(1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))*(1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))))", (-0.99960, -0.16333));
color(ast0, #6b2f5e); stroke(ast0, 3); untraced(ast0);
plot(ast1, (409.00, 878.00), 102.1857, 102.1857, "sqrt(abs((1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))*(1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))*(1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))))", (0.16333, 0.99960));
color(ast1, #6b2f5e); stroke(ast1, 3); untraced(ast1);
plot(ast2, (409.00, 878.00), 102.1857, 102.1857, "0 - sqrt(abs((1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))*(1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))*(1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))))", (-0.99960, -0.16333));
color(ast2, #6b2f5e); stroke(ast2, 3); untraced(ast2);
plot(ast3, (409.00, 878.00), 102.1857, 102.1857, "0 - sqrt(abs((1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))*(1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))*(1 - exp(0.6666666667*ln(abs(x) + 0.000000001)))))", (0.16333, 0.99960));
color(ast3, #6b2f5e); stroke(ast3, 3); untraced(ast3);
equation(q_ast, (409.0, 956.0), `y^2=(1-x^{2/3})^3`, 16);
color(q_ast, #6b2f5e); hidden(q_ast);
text(n_ast, (409.0, 978.0), "astroïde");
size(n_ast, 11); color(n_ast, ink); opacity(n_ast, 0.8); hidden(n_ast);
coords(k_dbt, (673.00, 878.00), (-0.9996, 0.9996), (-0.3848, 0.3848), 104.0416, 104.0416, 0, 0.25, 0);
color(k_dbt, slate); opacity(k_dbt, 0.38);
plot(dbt0, (673.00, 878.00), 104.0416, 104.0416, "sqrt(abs(x*x*x*x*(1 - x*x)))", (-0.99960, -0.82133));
color(dbt0, #6b2f5e); stroke(dbt0, 3); untraced(dbt0);
plot(dbt1, (673.00, 878.00), 104.0416, 104.0416, "sqrt(abs(x*x*x*x*(1 - x*x)))", (-0.81107, 0.81107));
color(dbt1, #6b2f5e); stroke(dbt1, 3); untraced(dbt1);
plot(dbt2, (673.00, 878.00), 104.0416, 104.0416, "sqrt(abs(x*x*x*x*(1 - x*x)))", (0.82133, 0.99960));
color(dbt2, #6b2f5e); stroke(dbt2, 3); untraced(dbt2);
plot(dbt3, (673.00, 878.00), 104.0416, 104.0416, "0 - sqrt(abs(x*x*x*x*(1 - x*x)))", (-0.99960, -0.82133));
color(dbt3, #6b2f5e); stroke(dbt3, 3); untraced(dbt3);
plot(dbt4, (673.00, 878.00), 104.0416, 104.0416, "0 - sqrt(abs(x*x*x*x*(1 - x*x)))", (-0.81107, 0.81107));
color(dbt4, #6b2f5e); stroke(dbt4, 3); untraced(dbt4);
plot(dbt5, (673.00, 878.00), 104.0416, 104.0416, "0 - sqrt(abs(x*x*x*x*(1 - x*x)))", (0.82133, 0.99960));
color(dbt5, #6b2f5e); stroke(dbt5, 3); untraced(dbt5);
equation(q_dbt, (673.0, 956.0), `y^2=x^4(1-x^2)`, 16);
color(q_dbt, #6b2f5e); hidden(q_dbt);
text(n_dbt, (673.0, 978.0), "double goutte d'eau");
size(n_dbt, 11); color(n_dbt, ink); opacity(n_dbt, 0.8); hidden(n_dbt);
coords(k_bif, (878.72, 878.00), (0.0000, 0.6623), (-0.3409, 0.3409), 175.9921, 175.9921, 0, 0.25, 0);
color(k_bif, slate); opacity(k_bif, 0.38);
plot(bif0, (878.72, 878.00), 175.9921, 175.9921, "sqrt(abs(x)) + sqrt(abs(x*(1 - x)))", (0.00000, 0.02933));
color(bif0, #6b2f5e); stroke(bif0, 3); untraced(bif0);
plot(bif1, (878.72, 878.00), 175.9921, 175.9921, "sqrt(abs(x)) - sqrt(abs(x*(1 - x)))", (0.00000, 0.66233));
color(bif1, #6b2f5e); stroke(bif1, 3); untraced(bif1);
plot(bif2, (878.72, 878.00), 175.9921, 175.9921, "0 - sqrt(abs(x)) + sqrt(abs(x*(1 - x)))", (0.00000, 0.66233));
color(bif2, #6b2f5e); stroke(bif2, 3); untraced(bif2);
plot(bif3, (878.72, 878.00), 175.9921, 175.9921, "0 - sqrt(abs(x)) - sqrt(abs(x*(1 - x)))", (0.00000, 0.02933));
color(bif3, #6b2f5e); stroke(bif3, 3); untraced(bif3);
equation(q_bif, (937.0, 956.0), `y=\pm\sqrt{x}\pm\sqrt{x(1-x)}`, 16);
color(q_bif, #6b2f5e); hidden(q_bif);
text(n_bif, (937.0, 978.0), "bifolium");
size(n_bif, 11); color(n_bif, ink); opacity(n_bif, 0.8); hidden(n_bif);
coords(k_agn, (145.00, 1087.68), (-3.2000, 3.2000), (0.0890, 0.9990), 32.5000, 32.5000, 0, 1.0, 0);
color(k_agn, slate); opacity(k_agn, 0.38);
plot(agn0, (145.00, 1087.68), 32.5000, 32.5000, "1/(1 + x*x)", (-3.20000, -0.03200));
color(agn0, #8a6a1f); stroke(agn0, 3); untraced(agn0);
plot(agn1, (145.00, 1087.68), 32.5000, 32.5000, "1/(1 + x*x)", (0.03200, 3.20000));
color(agn1, #8a6a1f); stroke(agn1, 3); untraced(agn1);
equation(q_agn, (145.0, 1148.0), `y=\tfrac{1}{1+x^2}`, 16);
color(q_agn, #8a6a1f); hidden(q_agn);
text(n_agn, (145.0, 1170.0), "cubique d'Agnesi");
size(n_agn, 11); color(n_agn, ink); opacity(n_agn, 0.8); hidden(n_agn);
coords(k_ang, (409.00, 1070.00), (-3.2000, 3.2000), (-0.4999, 0.4999), 32.5000, 32.5000, 0, 1.0, 0);
color(k_ang, slate); opacity(k_ang, 0.38);
plot(ang0, (409.00, 1070.00), 32.5000, 32.5000, "x/(1 + x*x)", (-3.20000, -1.01760));
color(ang0, #8a6a1f); stroke(ang0, 3); untraced(ang0);
plot(ang1, (409.00, 1070.00), 32.5000, 32.5000, "x/(1 + x*x)", (-0.98347, 0.98347));
color(ang1, #8a6a1f); stroke(ang1, 3); untraced(ang1);
plot(ang2, (409.00, 1070.00), 32.5000, 32.5000, "x/(1 + x*x)", (1.01760, 3.20000));
color(ang2, #8a6a1f); stroke(ang2, 3); untraced(ang2);
equation(q_ang, (409.0, 1148.0), `y=\tfrac{x}{1+x^2}`, 16);
color(q_ang, #8a6a1f); hidden(q_ang);
text(n_ang, (409.0, 1170.0), "anguinée");
size(n_ang, 11); color(n_ang, ink); opacity(n_ang, 0.8); hidden(n_ang);
coords(k_tri, (673.38, 1070.00), (-3.2000, 3.1595), (-3.2092, 3.2092), 18.6965, 18.6965, 0, 1.0, 0);
color(k_tri, slate); opacity(k_tri, 0.38);
plot(tri0, (673.38, 1070.00), 18.6965, 18.6965, "sqrt(abs((x*x*x + 1)/x))", (-3.20000, -1.00053));
color(tri0, #8a6a1f); stroke(tri0, 3); untraced(tri0);
plot(tri1, (673.38, 1070.00), 18.6965, 18.6965, "sqrt(abs((x*x*x + 1)/x))", (0.09813, 3.15947));
color(tri1, #8a6a1f); stroke(tri1, 3); untraced(tri1);
plot(tri2, (673.38, 1070.00), 18.6965, 18.6965, "0 - sqrt(abs((x*x*x + 1)/x))", (-3.20000, -1.00053));
color(tri2, #8a6a1f); stroke(tri2, 3); untraced(tri2);
plot(tri3, (673.38, 1070.00), 18.6965, 18.6965, "0 - sqrt(abs((x*x*x + 1)/x))", (0.09813, 3.15947));
color(tri3, #8a6a1f); stroke(tri3, 3); untraced(tri3);
equation(q_tri, (673.0, 1148.0), `y^2=\tfrac{x^3+1}{x}`, 16);
color(q_tri, #8a6a1f); hidden(q_tri);
text(n_tri, (673.0, 1170.0), "trident de Newton");
size(n_tri, 11); color(n_tri, ink); opacity(n_tri, 0.8); hidden(n_tri);
coords(k_mix, (904.02, 1070.00), (0.0000, 3.2000), (-2.9106, 2.9106), 20.6144, 20.6144, 0, 1.0, 0);
color(k_mix, slate); opacity(k_mix, 0.38);
plot(mix0, (904.02, 1070.00), 20.6144, 20.6144, "sqrt(abs(x*x/(x - 1)))", (1.15840, 3.20000));
color(mix0, #8a6a1f); stroke(mix0, 3); untraced(mix0);
plot(mix1, (904.02, 1070.00), 20.6144, 20.6144, "0 - sqrt(abs(x*x/(x - 1)))", (1.15840, 3.20000));
color(mix1, #8a6a1f); stroke(mix1, 3); untraced(mix1);
circle(ac_mix0, (904.02, 1070.00), 4.5); color(ac_mix0, #8a6a1f); hidden(ac_mix0);
equation(q_mix, (937.0, 1148.0), `y^2=\tfrac{x^2}{x-1}`, 16);
color(q_mix, #8a6a1f); hidden(q_mix);
text(n_mix, (937.0, 1170.0), "cubique mixte");
size(n_mix, 11); color(n_mix, ink); opacity(n_mix, 0.8); hidden(n_mix);
coords(k_cis, (109.92, 1262.00), (0.0000, 0.4224), (-0.3612, 0.3612), 166.1036, 166.1036, 0, 0.25, 0);
color(k_cis, slate); opacity(k_cis, 0.38);
plot(cis0, (109.92, 1262.00), 166.1036, 166.1036, "sqrt(abs(x*x*x/(1 - x)))", (0.00000, 0.42240));
color(cis0, #8a6a1f); stroke(cis0, 3); untraced(cis0);
plot(cis1, (109.92, 1262.00), 166.1036, 166.1036, "0 - sqrt(abs(x*x*x/(1 - x)))", (0.00000, 0.42240));
color(cis1, #8a6a1f); stroke(cis1, 3); untraced(cis1);
equation(q_cis, (145.0, 1340.0), `y^2=\tfrac{x^3}{1-x}`, 16);
color(q_cis, #8a6a1f); hidden(q_cis);
text(n_cis, (145.0, 1362.0), "cissoïde de Dioclès");
size(n_cis, 11); color(n_cis, ink); opacity(n_cis, 0.8); hidden(n_cis);
coords(k_duu, (409.00, 1262.00), (-0.9592, 0.9592), (-3.5370, 3.5370), 16.9637, 16.9637, 0, 1.0, 0);
color(k_duu, slate); opacity(k_duu, 0.38);
plot(duu0, (409.00, 1262.00), 16.9637, 16.9637, "sqrt(abs(1/(1 - x*x)))", (-0.95920, 0.95920));
color(duu0, #1f5f66); stroke(duu0, 3); untraced(duu0);
plot(duu1, (409.00, 1262.00), 16.9637, 16.9637, "0 - sqrt(abs(1/(1 - x*x)))", (-0.95920, 0.95920));
color(duu1, #1f5f66); stroke(duu1, 3); untraced(duu1);
equation(q_duu, (409.0, 1340.0), `y^2=\tfrac{1}{1-x^2}`, 16);
color(q_duu, #1f5f66); hidden(q_duu);
text(n_duu, (409.0, 1362.0), "double u");
size(n_duu, 11); color(n_duu, ink); opacity(n_duu, 0.8); hidden(n_duu);
coords(k_pun, (673.00, 1262.00), (-0.5911, 0.5911), (-0.7328, 0.7328), 81.8814, 81.8814, 0, 0.25, 0);
color(k_pun, slate); opacity(k_pun, 0.38);
plot(pun0, (673.00, 1262.00), 81.8814, 81.8814, "sqrt(abs(x*x/(1 - x*x)))", (-0.59107, 0.59107));
color(pun0, #1f5f66); stroke(pun0, 3); untraced(pun0);
plot(pun1, (673.00, 1262.00), 81.8814, 81.8814, "0 - sqrt(abs(x*x/(1 - x*x)))", (-0.59107, 0.59107));
color(pun1, #1f5f66); stroke(pun1, 3); untraced(pun1);
equation(q_pun, (673.0, 1340.0), `y^2=\tfrac{x^2}{1-x^2}`, 16);
color(q_pun, #1f5f66); hidden(q_pun);
text(n_pun, (673.0, 1362.0), "puntiforme");
size(n_pun, 11); color(n_pun, ink); opacity(n_pun, 0.8); hidden(n_pun);
coords(k_cru, (937.00, 1262.00), (-3.2000, 3.2000), (-1.8433, 1.8433), 32.5000, 32.5000, 0, 1.0, 0);
color(k_cru, slate); opacity(k_cru, 0.38);
plot(cru0, (937.00, 1262.00), 32.5000, 32.5000, "sqrt(abs(x*x/(x*x - 1)))", (-3.20000, -1.19253));
color(cru0, #1f5f66); stroke(cru0, 3); untraced(cru0);
plot(cru1, (937.00, 1262.00), 32.5000, 32.5000, "sqrt(abs(x*x/(x*x - 1)))", (1.19040, 3.20000));
color(cru1, #1f5f66); stroke(cru1, 3); untraced(cru1);
plot(cru2, (937.00, 1262.00), 32.5000, 32.5000, "0 - sqrt(abs(x*x/(x*x - 1)))", (-3.20000, -1.19253));
color(cru2, #1f5f66); stroke(cru2, 3); untraced(cru2);
plot(cru3, (937.00, 1262.00), 32.5000, 32.5000, "0 - sqrt(abs(x*x/(x*x - 1)))", (1.19040, 3.20000));
color(cru3, #1f5f66); stroke(cru3, 3); untraced(cru3);
circle(ac_cru0, (937.00, 1262.00), 4.5); color(ac_cru0, #1f5f66); hidden(ac_cru0);
equation(q_cru, (937.0, 1340.0), `y^2=\tfrac{x^2}{x^2-1}`, 16);
color(q_cru, #1f5f66); hidden(q_cru);
text(n_cru, (937.0, 1362.0), "cruciforme");
size(n_cru, 11); color(n_cru, ink); opacity(n_cru, 0.8); hidden(n_cru);
coords(k_kul, (145.00, 1454.00), (-3.2000, 3.2000), (-0.9995, 0.9995), 32.5000, 32.5000, 0, 1.0, 0);
color(k_kul, slate); opacity(k_kul, 0.38);
plot(kul0, (145.00, 1454.00), 32.5000, 32.5000, "sqrt(abs(1/(x*x + 1)))", (-3.20000, -0.03200));
color(kul0, #1f5f66); stroke(kul0, 3); untraced(kul0);
plot(kul1, (145.00, 1454.00), 32.5000, 32.5000, "sqrt(abs(1/(x*x + 1)))", (0.03200, 3.20000));
color(kul1, #1f5f66); stroke(kul1, 3); untraced(kul1);
plot(kul2, (145.00, 1454.00), 32.5000, 32.5000, "0 - sqrt(abs(1/(x*x + 1)))", (-3.20000, -0.03200));
color(kul2, #1f5f66); stroke(kul2, 3); untraced(kul2);
plot(kul3, (145.00, 1454.00), 32.5000, 32.5000, "0 - sqrt(abs(1/(x*x + 1)))", (0.03200, 3.20000));
color(kul3, #1f5f66); stroke(kul3, 3); untraced(kul3);
equation(q_kul, (145.0, 1532.0), `y^2=\tfrac{1}{x^2+1}`, 16);
color(q_kul, #1f5f66); hidden(q_kul);
text(n_kul, (145.0, 1554.0), "quartique de Külp");
size(n_kul, 11); color(n_kul, ink); opacity(n_kul, 0.8); hidden(n_kul);
coords(k_cap, (409.00, 1454.00), (-0.4987, 0.4987), (-0.2869, 0.2869), 208.5561, 208.5561, 0, 0.25, 0);
color(k_cap, slate); opacity(k_cap, 0.38);
plot(cap0, (409.00, 1454.00), 208.5561, 208.5561, "sqrt(abs(x*x*x*x/(1 - x*x)))", (-0.49867, 0.49867));
color(cap0, #1f5f66); stroke(cap0, 3); untraced(cap0);
plot(cap1, (409.00, 1454.00), 208.5561, 208.5561, "0 - sqrt(abs(x*x*x*x/(1 - x*x)))", (-0.49867, 0.49867));
color(cap1, #1f5f66); stroke(cap1, 3); untraced(cap1);
equation(q_cap, (409.0, 1532.0), `y^2=\tfrac{x^4}{1-x^2}`, 16);
color(q_cap, #1f5f66); hidden(q_cap);
text(n_cap, (409.0, 1554.0), "cappa");
size(n_cap, 11); color(n_cap, ink); opacity(n_cap, 0.8); hidden(n_cap);
coords(k_str, (613.81, 1454.00), (-0.2743, 0.9988), (-0.3634, 0.3634), 163.3850, 163.3850, 0, 0.25, 0);
color(k_str, slate); opacity(k_str, 0.38);
plot(str0, (613.81, 1454.00), 163.3850, 163.3850, "sqrt(abs(x*x*(1 - x)/(1 + x)))", (-0.27427, 0.99880));
color(str0, #8f2033); stroke(str0, 3); untraced(str0);
plot(str1, (613.81, 1454.00), 163.3850, 163.3850, "0 - sqrt(abs(x*x*(1 - x)/(1 + x)))", (-0.27427, 0.99880));
color(str1, #8f2033); stroke(str1, 3); untraced(str1);
equation(q_str, (673.0, 1532.0), `y^2=x^2\tfrac{1-x}{1+x}`, 16);
color(q_str, #8f2033); hidden(q_str);
text(n_str, (673.0, 1554.0), "strophoïde");
size(n_str, 11); color(n_str, ink); opacity(n_str, 0.8); hidden(n_str);
coords(k_tre, (899.20, 1454.00), (-1.2012, 2.5931), (-1.1046, 1.1046), 54.3185, 54.3185, 0, 0.5, 0);
color(k_tre, slate); opacity(k_tre, 0.38);
plot(tre0, (899.20, 1454.00), 54.3185, 54.3185, "sqrt(abs(x*x*(x - 1)/(3*x + 1)))", (-1.20120, -0.39173));
color(tre0, #8f2033); stroke(tre0, 3); untraced(tre0);
plot(tre1, (899.20, 1454.00), 54.3185, 54.3185, "sqrt(abs(x*x*(x - 1)/(3*x + 1)))", (1.00013, 2.59307));
color(tre1, #8f2033); stroke(tre1, 3); untraced(tre1);
plot(tre2, (899.20, 1454.00), 54.3185, 54.3185, "0 - sqrt(abs(x*x*(x - 1)/(3*x + 1)))", (-1.20120, -0.39173));
color(tre2, #8f2033); stroke(tre2, 3); untraced(tre2);
plot(tre3, (899.20, 1454.00), 54.3185, 54.3185, "0 - sqrt(abs(x*x*(x - 1)/(3*x + 1)))", (1.00013, 2.59307));
color(tre3, #8f2033); stroke(tre3, 3); untraced(tre3);
circle(ac_tre0, (899.20, 1454.00), 4.5); color(ac_tre0, #8f2033); hidden(ac_tre0);
equation(q_tre, (937.0, 1532.0), `y^2=x^2\tfrac{x-1}{3x+1}`, 16);
color(q_tre, #8f2033); hidden(q_tre);
text(n_tre, (937.0, 1554.0), "trèfle équilatère");
size(n_tre, 11); color(n_tre, ink); opacity(n_tre, 0.8); hidden(n_tre);
coords(k_vis, (100.46, 1646.00), (0.0000, 2.0000), (-1.3471, 1.3471), 44.5412, 44.5412, 0, 0.5, 0);
color(k_vis, slate); opacity(k_vis, 0.38);
plot(vis0, (100.46, 1646.00), 44.5412, 44.5412, "sqrt(abs(x*x*(2 - x)/(x - 1)))", (1.57867, 2.00000));
color(vis0, #8f2033); stroke(vis0, 3); untraced(vis0);
plot(vis1, (100.46, 1646.00), 44.5412, 44.5412, "0 - sqrt(abs(x*x*(2 - x)/(x - 1)))", (1.57867, 2.00000));
color(vis1, #8f2033); stroke(vis1, 3); untraced(vis1);
circle(ac_vis0, (100.46, 1646.00), 4.5); color(ac_vis0, #8f2033); hidden(ac_vis0);
equation(q_vis, (145.0, 1724.0), `y^2=x^2\tfrac{2-x}{x-1}`, 16);
color(q_vis, #8f2033); hidden(q_vis);
text(n_vis, (145.0, 1746.0), "visiera");
size(n_vis, 11); color(n_vis, ink); opacity(n_vis, 0.8); hidden(n_vis);
coords(k_mac, (330.01, 1646.00), (-0.4078, 2.9987), (-0.9840, 0.9840), 60.9757, 60.9757, 0, 0.5, 0);
color(k_mac, slate); opacity(k_mac, 0.38);
plot(mac0, (330.01, 1646.00), 60.9757, 60.9757, "sqrt(abs(x*x*(3 - x)/(1 + x)))", (-0.40780, 0.96873));
color(mac0, #8f2033); stroke(mac0, 3); untraced(mac0);
plot(mac1, (330.01, 1646.00), 60.9757, 60.9757, "sqrt(abs(x*x*(3 - x)/(1 + x)))", (2.44193, 2.99873));
color(mac1, #8f2033); stroke(mac1, 3); untraced(mac1);
plot(mac2, (330.01, 1646.00), 60.9757, 60.9757, "0 - sqrt(abs(x*x*(3 - x)/(1 + x)))", (-0.40780, 0.96873));
color(mac2, #8f2033); stroke(mac2, 3); untraced(mac2);
plot(mac3, (330.01, 1646.00), 60.9757, 60.9757, "0 - sqrt(abs(x*x*(3 - x)/(1 + x)))", (2.44193, 2.99873));
color(mac3, #8f2033); stroke(mac3, 3); untraced(mac3);
equation(q_mac, (409.0, 1724.0), `y^2=x^2\tfrac{3-x}{1+x}`, 16);
color(q_mac, #8f2033); hidden(q_mac);
text(n_mac, (409.0, 1746.0), "trisectrice de Maclaurin");
size(n_mac, 11); color(n_mac, ink); opacity(n_mac, 0.8); hidden(n_mac);
coords(k_bic, (673.04, 1697.99), (-1.0000, 0.9992), (0.0000, 0.9994), 104.0416, 104.0416, 0, 0.25, 0);
color(k_bic, slate); opacity(k_bic, 0.38);
plot(bic0, (673.04, 1697.99), 104.0416, 104.0416, "(1 - x*x)/(2 + sqrt(abs(1 - x*x)))", (-1.00000, 0.99920));
color(bic0, #8f2033); stroke(bic0, 3); untraced(bic0);
plot(bic1, (673.04, 1697.99), 104.0416, 104.0416, "(1 - x*x)/(2 - sqrt(abs(1 - x*x)))", (-1.00000, -0.02000));
color(bic1, #8f2033); stroke(bic1, 3); untraced(bic1);
plot(bic2, (673.04, 1697.99), 104.0416, 104.0416, "(1 - x*x)/(2 - sqrt(abs(1 - x*x)))", (0.02000, 0.99920));
color(bic2, #8f2033); stroke(bic2, 3); untraced(bic2);
equation(q_bic, (673.0, 1724.0), `y=\tfrac{1-x^2}{2\pm\sqrt{1-x^2}}`, 16);
color(q_bic, #8f2033); hidden(q_bic);
text(n_bic, (673.0, 1746.0), "bicorne");
size(n_bic, 11); color(n_bic, ink); opacity(n_bic, 0.8); hidden(n_bic);
coords(k_hum, (933.69, 1646.00), (-2.4180, 2.5775), (-1.4446, 1.4446), 41.5350, 41.5350, 0, 0.5, 0);
color(k_hum, slate); opacity(k_hum, 0.38);
plot(hum0, (933.69, 1646.00), 41.5350, 41.5350, "sqrt(abs((x*x*x - 1)/(3*x)))", (-2.41800, -0.16120));
color(hum0, #3a4a7a); stroke(hum0, 3); untraced(hum0);
plot(hum1, (933.69, 1646.00), 41.5350, 41.5350, "sqrt(abs((x*x*x - 1)/(3*x)))", (1.00013, 2.57747));
color(hum1, #3a4a7a); stroke(hum1, 3); untraced(hum1);
plot(hum2, (933.69, 1646.00), 41.5350, 41.5350, "0 - sqrt(abs((x*x*x - 1)/(3*x)))", (-2.41800, -0.16120));
color(hum2, #3a4a7a); stroke(hum2, 3); untraced(hum2);
plot(hum3, (933.69, 1646.00), 41.5350, 41.5350, "0 - sqrt(abs((x*x*x - 1)/(3*x)))", (1.00013, 2.57747));
color(hum3, #3a4a7a); stroke(hum3, 3); untraced(hum3);
equation(q_hum, (937.0, 1724.0), `y^2=\tfrac{x^3-1}{3x}`, 16);
color(q_hum, #3a4a7a); hidden(q_hum);
text(n_hum, (937.0, 1746.0), "cubique de Humbert");
size(n_hum, 11); color(n_hum, ink); opacity(n_hum, 0.8); hidden(n_hum);
seq {
// 1 — the five basics
par {
seq { wait(0.00); draw(par0, 1.5); }
seq { wait(0.50); show(q_par, 0.4); show(n_par, 0.4); }
seq { wait(0.30); draw(hyp0, 1.5); }
seq { wait(0.30); draw(hyp1, 1.5); }
seq { wait(0.80); show(q_hyp, 0.4); show(n_hyp, 0.4); }
seq { wait(0.60); draw(sem0, 1.5); }
seq { wait(0.60); draw(sem1, 1.5); }
seq { wait(1.10); show(q_sem, 0.4); show(n_sem, 0.4); }
seq { wait(0.90); draw(dup0, 1.5); }
seq { wait(0.90); draw(dup1, 1.5); }
seq { wait(2.00); show(ac_dup0, 0.4); }
seq { wait(1.40); show(q_dup, 0.4); show(n_dup, 0.4); }
seq { wait(1.20); draw(div0, 1.5); }
seq { wait(1.20); draw(div1, 1.5); }
seq { wait(1.20); draw(div2, 1.5); }
seq { wait(1.20); draw(div3, 1.5); }
seq { wait(1.70); show(q_div, 0.4); show(n_div, 0.4); }
}
// 2 — Lame, and fractional exponents
par {
seq { wait(0.00); draw(lam0, 1.5); }
seq { wait(0.50); show(q_lam, 0.4); show(n_lam, 0.4); }
seq { wait(0.30); draw(dip0, 1.5); }
seq { wait(0.30); draw(dip1, 1.5); }
seq { wait(0.30); draw(dip2, 1.5); }
seq { wait(0.30); draw(dip3, 1.5); }
seq { wait(0.30); draw(dip4, 1.5); }
seq { wait(0.30); draw(dip5, 1.5); }
seq { wait(0.80); show(q_dip, 0.4); show(n_dip, 0.4); }
seq { wait(0.60); draw(oeu0, 1.5); }
seq { wait(0.60); draw(oeu1, 1.5); }
seq { wait(0.60); draw(oeu2, 1.5); }
seq { wait(0.60); draw(oeu3, 1.5); }
seq { wait(0.60); draw(oeu4, 1.5); }
seq { wait(0.60); draw(oeu5, 1.5); }
seq { wait(1.10); show(q_oeu, 0.4); show(n_oeu, 0.4); }
seq { wait(0.90); draw(fos0, 1.5); }
seq { wait(0.90); draw(fos1, 1.5); }
seq { wait(0.90); draw(fos2, 1.5); }
seq { wait(0.90); draw(fos3, 1.5); }
seq { wait(1.40); show(q_fos, 0.4); show(n_fos, 0.4); }
}
// 3 — squares against squares
par {
seq { wait(0.00); draw(hui0, 1.5); }
seq { wait(0.00); draw(hui1, 1.5); }
seq { wait(0.00); draw(hui2, 1.5); }
seq { wait(0.00); draw(hui3, 1.5); }
seq { wait(0.00); draw(hui4, 1.5); }
seq { wait(0.00); draw(hui5, 1.5); }
seq { wait(0.50); show(q_hui, 0.4); show(n_hui, 0.4); }
seq { wait(0.30); draw(pir0, 1.5); }
seq { wait(0.30); draw(pir1, 1.5); }
seq { wait(0.30); draw(pir2, 1.5); }
seq { wait(0.30); draw(pir3, 1.5); }
seq { wait(0.80); show(q_pir, 0.4); show(n_pir, 0.4); }
seq { wait(0.60); draw(cam0, 1.5); }
seq { wait(0.60); draw(cam1, 1.5); }
seq { wait(0.60); draw(cam2, 1.5); }
seq { wait(0.60); draw(cam3, 1.5); }
seq { wait(1.70); show(ac_cam0, 0.4); }
seq { wait(1.10); show(q_cam, 0.4); show(n_cam, 0.4); }
}
// 4 — cubes, and four branches
par {
seq { wait(0.00); draw(bou0, 1.5); }
seq { wait(0.00); draw(bou1, 1.5); }
seq { wait(0.00); draw(bou2, 1.5); }
seq { wait(0.00); draw(bou3, 1.5); }
seq { wait(0.50); show(q_bou, 0.4); show(n_bou, 0.4); }
seq { wait(0.30); draw(ast0, 1.5); }
seq { wait(0.30); draw(ast1, 1.5); }
seq { wait(0.30); draw(ast2, 1.5); }
seq { wait(0.30); draw(ast3, 1.5); }
seq { wait(0.80); show(q_ast, 0.4); show(n_ast, 0.4); }
seq { wait(0.60); draw(dbt0, 1.5); }
seq { wait(0.60); draw(dbt1, 1.5); }
seq { wait(0.60); draw(dbt2, 1.5); }
seq { wait(0.60); draw(dbt3, 1.5); }
seq { wait(0.60); draw(dbt4, 1.5); }
seq { wait(0.60); draw(dbt5, 1.5); }
seq { wait(1.10); show(q_dbt, 0.4); show(n_dbt, 0.4); }
seq { wait(0.90); draw(bif0, 1.5); }
seq { wait(0.90); draw(bif1, 1.5); }
seq { wait(0.90); draw(bif2, 1.5); }
seq { wait(0.90); draw(bif3, 1.5); }
seq { wait(1.40); show(q_bif, 0.4); show(n_bif, 0.4); }
}
// 5 — quotients that stay finite
par {
seq { wait(0.00); draw(agn0, 1.5); }
seq { wait(0.00); draw(agn1, 1.5); }
seq { wait(0.50); show(q_agn, 0.4); show(n_agn, 0.4); }
seq { wait(0.30); draw(ang0, 1.5); }
seq { wait(0.30); draw(ang1, 1.5); }
seq { wait(0.30); draw(ang2, 1.5); }
seq { wait(0.80); show(q_ang, 0.4); show(n_ang, 0.4); }
seq { wait(0.60); draw(tri0, 1.5); }
seq { wait(0.60); draw(tri1, 1.5); }
seq { wait(0.60); draw(tri2, 1.5); }
seq { wait(0.60); draw(tri3, 1.5); }
seq { wait(1.10); show(q_tri, 0.4); show(n_tri, 0.4); }
seq { wait(0.90); draw(mix0, 1.5); }
seq { wait(0.90); draw(mix1, 1.5); }
seq { wait(2.00); show(ac_mix0, 0.4); }
seq { wait(1.40); show(q_mix, 0.4); show(n_mix, 0.4); }
seq { wait(1.20); draw(cis0, 1.5); }
seq { wait(1.20); draw(cis1, 1.5); }
seq { wait(1.70); show(q_cis, 0.4); show(n_cis, 0.4); }
}
// 6 — quotients with poles
par {
seq { wait(0.00); draw(duu0, 1.5); }
seq { wait(0.00); draw(duu1, 1.5); }
seq { wait(0.50); show(q_duu, 0.4); show(n_duu, 0.4); }
seq { wait(0.30); draw(pun0, 1.5); }
seq { wait(0.30); draw(pun1, 1.5); }
seq { wait(0.80); show(q_pun, 0.4); show(n_pun, 0.4); }
seq { wait(0.60); draw(cru0, 1.5); }
seq { wait(0.60); draw(cru1, 1.5); }
seq { wait(0.60); draw(cru2, 1.5); }
seq { wait(0.60); draw(cru3, 1.5); }
seq { wait(1.70); show(ac_cru0, 0.4); }
seq { wait(1.10); show(q_cru, 0.4); show(n_cru, 0.4); }
seq { wait(0.90); draw(kul0, 1.5); }
seq { wait(0.90); draw(kul1, 1.5); }
seq { wait(0.90); draw(kul2, 1.5); }
seq { wait(0.90); draw(kul3, 1.5); }
seq { wait(1.40); show(q_kul, 0.4); show(n_kul, 0.4); }
seq { wait(1.20); draw(cap0, 1.5); }
seq { wait(1.20); draw(cap1, 1.5); }
seq { wait(1.70); show(q_cap, 0.4); show(n_cap, 0.4); }
}
// 7 — the a/b family
par {
seq { wait(0.00); draw(str0, 1.5); }
seq { wait(0.00); draw(str1, 1.5); }
seq { wait(0.50); show(q_str, 0.4); show(n_str, 0.4); }
seq { wait(0.30); draw(tre0, 1.5); }
seq { wait(0.30); draw(tre1, 1.5); }
seq { wait(0.30); draw(tre2, 1.5); }
seq { wait(0.30); draw(tre3, 1.5); }
seq { wait(1.40); show(ac_tre0, 0.4); }
seq { wait(0.80); show(q_tre, 0.4); show(n_tre, 0.4); }
seq { wait(0.60); draw(vis0, 1.5); }
seq { wait(0.60); draw(vis1, 1.5); }
seq { wait(1.70); show(ac_vis0, 0.4); }
seq { wait(1.10); show(q_vis, 0.4); show(n_vis, 0.4); }
seq { wait(0.90); draw(mac0, 1.5); }
seq { wait(0.90); draw(mac1, 1.5); }
seq { wait(0.90); draw(mac2, 1.5); }
seq { wait(0.90); draw(mac3, 1.5); }
seq { wait(1.40); show(q_mac, 0.4); show(n_mac, 0.4); }
seq { wait(1.20); draw(bic0, 1.5); }
seq { wait(1.20); draw(bic1, 1.5); }
seq { wait(1.20); draw(bic2, 1.5); }
seq { wait(1.70); show(q_bic, 0.4); show(n_bic, 0.4); }
}
// 8 — one last cubic
par {
seq { wait(0.00); draw(hum0, 1.5); }
seq { wait(0.00); draw(hum1, 1.5); }
seq { wait(0.00); draw(hum2, 1.5); }
seq { wait(0.00); draw(hum3, 1.5); }
seq { wait(0.50); show(q_hum, 0.4); show(n_hum, 0.4); }
}
}
wait(2.6);
araujo
A TORUS AND A CYLINDER, DRAWN THE WAY A DRAUGHTSMAN DRAWS THEM — scaffold first, object last, and nothing placed by eye. Orthographic, because a draughtsman’s scaffold is made of parallel lines and because the two hardest curves 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 n = (cos v cos u, sin v, cos v sin u), n·d = 0 solves in closed form, tan v = −(dx cos u + dz sin u)/dy — so the entire silhouette is TWO curve3 lines and nothing else (|n·d| = 5.7e-16). And it really is the outline: sampling the surface 360×180 and projecting along 72 directions, the surface never projects past those 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, n·L = 0 gives the terminator, slide each point down its light ray to z = 0 and that is the edge of the shadow (|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: substituting the cylinder into the torus gives √(x²+(z−z₀)²) = R ± √(r²−y²), so x² is known outright and x comes in ± pairs — six arc pieces that join into FOUR closed curves (488646 sampled points cluster into exactly 4 components), two on each side, x = 0 never reached so the sides never meet; every point sits on the torus AND the cylinder to 3.1e-15. The squares are the squares that inscribe the tube circles, the drop lines land under the points they came from, and the ticks are a real scale. After Rafael Araujo, whose method is that the construction IS the picture.
// 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);
developpante-de-cercle
THE INVOLUTE OF A CIRCLE, and the three machines made of it — mathcurve’s page, figure by figure. Hold a thread taut against a spool and unwind it; the end draws x = a(cos t + t sin t), y = a(sin t − t cos t). Arc length s = at²/2 and radius of curvature R = at, so R² = 2as and the TAUT THREAD IS THE RADIUS OF CURVATURE. ONE FACT CARRIES THE WHOLE PAGE: the normal at every point is a TANGENT to the base circle, of length exactly a·t (1.8e-15). Everything else follows. AUTOPARALLEL: push the involute along its own normals a distance d and you get the involute back, turned by d/a — 2.5e-15 at d = 0.7a, 1.1e-15 at πa, 3.6e-15 at 2πa. It is its own parallel at every offset and lands on ITSELF at d = 2πa; six copies are drawn a sixth of a turn apart and the seventh would be the first. GEARS: if the normal is always a base-circle tangent, two involutes in contact share a normal tangent to BOTH base circles — one fixed line, so the contact runs down a fixed LINE OF ACTION through a fixed PITCH POINT. Both flanks are solved and meet that line at the same point to 5.2e-14, counter-rotating at dψ/dφ = −0.625000000. Hence the reason every gear on earth has this tooth: two pairs are drawn at C = 3.0a and C = 3.6a, the pressure angle swings 29.93° → 43.76°, and the ratio reads 0.625000000 in both — MOVE INVOLUTE GEARS APART AND THE RATIO DOES NOT SHIFT. Here rb1:rb2 = 5:8, so the wheels carry 5 and 8 teeth. SCROLL COMPRESSOR (Léon Creux, 1905): the involute and the same involute turned by π — which by the autoparallel fact is the parallel at normal distance exactly πa, so one wall orbiting on a circle of radius πa stays in contact all the way round, cutting the gap into closed crescents that walk inward together. No valves, no reciprocating parts, one curve used twice. ARCHIMEDEAN LIMIT: ρ = a√(1+t²) and φ = t − arctan t, so ρ → a(φ + π/2) — an Archimedean spiral a quarter turn behind, the gap closing exactly like −a/2t (gap×2t = −0.910, −0.984, −0.996, −0.9997, −1.000000 at t = 2, 5, 10, 40, 1000). Its pitch is 2πa — the same 2πa the autoparallel check gave, which is why one curve makes both a gear and a spiral pump.
// developpante-de-cercle — the involute of a circle, and the three machines that are
// made of it. Following mathcurve's page, figure by figure.
//
// ONE CURVE: hold a thread taut against a spool and unwind it. The end draws the
// INVOLUTE. Everything below is that, and nothing else.
//
// x = a(cos t + t sin t), y = a(sin t − t cos t)
//
// WHAT IS CHECKED HERE, and to what:
//
// arc length s = a·t²/2 exact
// curvature R = a·t — the radius of curvature IS the unwound thread
// intrinsic R² = 2as 1.000000000
// the normal at every point is TANGENT to the base circle 8.7e-10
// and the length of that normal is exactly a·t 1.8e-15
//
// THAT LAST LINE IS THE WHOLE PAGE. Every other panel is a consequence of it.
//
// AUTOPARALLEL (panel 3). Push the involute a distance d along its own normals and you
// get the involute again, turned by d/a — checked to 2.5e-15 at d = 0.7a, 1.1e-15 at
// d = πa, 3.6e-15 at d = 2πa. So the curve is its own parallel at EVERY offset, and it
// lands back on ITSELF at d = 2πa. Six copies are drawn a sixth of a turn apart; the
// seventh would be the first.
//
// GEARS (panel 4). If the normal at a contact point is always a tangent to the base
// circle, then two involutes touching share a normal tangent to BOTH base circles — and
// there is only one such line, fixed in space. So the contact runs along a fixed straight
// LINE OF ACTION and cuts the line of centres at a fixed PITCH POINT. Both flanks are
// solved here and they meet the line at the same point to 5.2e-14, and the base angles
// come out counter-rotating, dψ/dφ = −0.625000000, as an external pair must.
//
// The consequence is the reason every gear in the world has this tooth: the pitch ratio
// is rb1:rb2 whatever the centre distance. Two pairs are drawn, C = 3.0a and C = 3.6a.
// The pressure angle goes from 29.93° to 43.76° — and the ratio reads 0.625000000 in
// both. Move involute gears apart and the ratio does not shift. Here rb1:rb2 = 5:8, so
// the wheels carry 5 and 8 teeth and turn 85.7° against 53.6°.
//
// SCROLL COMPRESSOR (panel 5), Léon Creux, 1905. Take the involute and the SAME involute
// turned by π. By the autoparallel fact that half-turn copy is the parallel at normal
// distance exactly πa — so if one wall orbits (without rotating) on a circle of radius
// πa, the two walls stay in contact all the way round. The contacts cut the gap into
// closed crescents; turning the orbit walks every crescent inward at once. No valves and
// no reciprocating parts — one curve, used twice.
//
// THE ARCHIMEDEAN LIMIT (panel 6). ρ = a√(1+t²) and φ = t − arctan t, so far out
// ρ → a(φ + π/2): an Archimedean spiral, a quarter turn behind. The gap closes exactly
// like −a/(2t) — gap×2t = −0.910, −0.984, −0.996, −0.9997, −1.000000 at t = 2, 5, 10,
// 40, 1000. Its pitch is 2πa, the same 2πa the autoparallel check gave, which is why the
// involute makes both a gear and a spiral pump.
//
// manic examples/developpante-de-cercle.manic
title("The involute of a circle, and the three machines made of it");
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, "Unwind a thread from a spool", (540, 104), 26);
text(sub, (540, 146), "the normal is always a tangent to the spool — every panel below is that one fact");
size(sub, 16); color(sub, fg); opacity(sub, 0.62);
// ---- 1. the definition: the end of a taut unwinding thread ----------------------
circle(spool, (330.0, 392.0), 56.0);
outlined(spool); outline(spool, mint); stroke(spool, 3);
param(dinv, (330.0, 392.0), 56.0000, 56.0000, "cos(t) + t*sin(t)", "sin(t) - t*cos(t)", (0, 5.2));
color(dinv, gold); stroke(dinv, 4); untraced(dinv);
cloud(dthread, 300, #ffffff, 1.0) {
let go = min(t/5, 1)*5.2;
let u = i/300;
let tx = cos(go);
let ty = sin(go);
let x = 330.0 + 56.0*(tx + u*go*sin(go));
let y = 392.0 - 56.0*(ty - u*go*cos(go));
let sat = 0;
let r = 1.4;
let alpha = 0.75;
}
cloud(darc, 420, #ffffff, 1.0) {
let go = min(t/5, 1)*5.2;
let th = go*(i/420);
let x = 330.0 + 56.0*cos(th);
let y = 392.0 - 56.0*sin(th);
let hue = 190;
let sat = 0.8;
let r = 2.4;
}
cloud(dpen, 90, #ffffff, 1.0) {
let go = min(t/5, 1)*5.2;
let a = i/90*6.283185;
let rr = 7*(i/90);
let x = 330.0 + 56.0*(cos(go) + go*sin(go)) + rr*cos(a*9);
let y = 392.0 - 56.0*(sin(go) - go*cos(go)) + rr*sin(a*9);
let hue = 42;
let sat = 0.6;
let r = 2;
}
text(l1a, (790, 250), "the thread stays TANGENT to the spool");
size(l1a, 17); color(l1a, fg); hidden(l1a);
text(l1b, (790, 284), "and is exactly as long as the arc it left");
size(l1b, 17); color(l1b, cyan); hidden(l1b);
equation(e1a, (790, 342), `x=a(\cos t+t\sin t)`, 21); color(e1a, gold); hidden(e1a);
equation(e1b, (790, 386), `y=a(\sin t-t\cos t)`, 21); color(e1b, gold); hidden(e1b);
equation(e1c, (790, 452), `s=\tfrac{1}{2}at^2,\quad R=at`, 21); color(e1c, fg); hidden(e1c);
text(l1c, (790, 500), "R = at: the taut thread IS the radius of curvature");
size(l1c, 15); color(l1c, fg); opacity(l1c, 0.66); hidden(l1c);
// ---- 2. every normal is a tangent to the base circle ---------------------------
circle(nspool, (268.0, 800.0), 46.0);
outlined(nspool); outline(nspool, mint); stroke(nspool, 2); opacity(nspool, 0.8);
line(nr0, (311.37, 784.66), (316.58, 799.40));
color(nr0, cyan); stroke(nr0, 2); opacity(nr0, 0.5); untraced(nr0);
line(nr1, (307.39, 776.24), (320.29, 797.62));
color(nr1, cyan); stroke(nr1, 2); opacity(nr1, 0.5); untraced(nr1);
line(nr2, (301.79, 768.79), (325.07, 793.99));
color(nr2, cyan); stroke(nr2, 2); opacity(nr2, 0.5); untraced(nr2);
line(nr3, (294.81, 762.62), (330.27, 788.05));
color(nr3, cyan); stroke(nr3, 2); opacity(nr3, 0.5); untraced(nr3);
line(nr4, (286.73, 757.99), (335.11, 779.55));
color(nr4, cyan); stroke(nr4, 2); opacity(nr4, 0.5); untraced(nr4);
line(nr5, (277.88, 755.07), (338.72, 768.46));
color(nr5, cyan); stroke(nr5, 2); opacity(nr5, 0.5); untraced(nr5);
line(nr6, (268.63, 754.00), (340.25, 754.98));
color(nr6, cyan); stroke(nr6, 2); opacity(nr6, 0.5); untraced(nr6);
line(nr7, (259.35, 754.82), (338.86, 739.59));
color(nr7, cyan); stroke(nr7, 2); opacity(nr7, 0.5); untraced(nr7);
line(nr8, (250.42, 757.49), (333.86, 722.99));
color(nr8, cyan); stroke(nr8, 2); opacity(nr8, 0.5); untraced(nr8);
line(nr9, (242.22, 761.90), (324.73, 706.07));
color(nr9, cyan); stroke(nr9, 2); opacity(nr9, 0.5); untraced(nr9);
line(nr10, (235.07, 767.88), (311.16, 689.89));
color(nr10, cyan); stroke(nr10, 2); opacity(nr10, 0.5); untraced(nr10);
line(nr11, (229.28, 775.17), (293.12, 675.60));
color(nr11, cyan); stroke(nr11, 2); opacity(nr11, 0.5); untraced(nr11);
line(nr12, (225.07, 783.48), (270.90, 664.38));
color(nr12, cyan); stroke(nr12, 2); opacity(nr12, 0.5); untraced(nr12);
line(nr13, (222.62, 792.47), (245.04, 657.37));
color(nr13, cyan); stroke(nr13, 2); opacity(nr13, 0.5); untraced(nr13);
line(nr14, (222.03, 801.77), (216.42, 655.59));
color(nr14, cyan); stroke(nr14, 2); opacity(nr14, 0.5); untraced(nr14);
line(nr15, (223.33, 810.99), (186.15, 659.89));
color(nr15, cyan); stroke(nr15, 2); opacity(nr15, 0.5); untraced(nr15);
line(nr16, (226.46, 819.76), (155.59, 670.82));
color(nr16, cyan); stroke(nr16, 2); opacity(nr16, 0.5); untraced(nr16);
line(nr17, (231.30, 827.73), (126.25, 688.67));
color(nr17, cyan); stroke(nr17, 2); opacity(nr17, 0.5); untraced(nr17);
line(nr18, (237.64, 834.55), (99.72, 713.35));
color(nr18, cyan); stroke(nr18, 2); opacity(nr18, 0.5); untraced(nr18);
line(nr19, (245.22, 839.96), (77.60, 744.41));
color(nr19, cyan); stroke(nr19, 2); opacity(nr19, 0.5); untraced(nr19);
line(nr20, (253.74, 843.73), (61.44, 781.02));
color(nr20, cyan); stroke(nr20, 2); opacity(nr20, 0.5); untraced(nr20);
line(nr21, (262.84, 845.71), (52.58, 821.98));
color(nr21, cyan); stroke(nr21, 2); opacity(nr21, 0.5); untraced(nr21);
param(ninv, (268.0, 800.0), 46.0000, 46.0000, "cos(t) + t*sin(t)", "sin(t) - t*cos(t)", (0, 4.6));
color(ninv, gold); stroke(ninv, 4); untraced(ninv);
text(l2, (268, 918), "22 normals, 22 tangents");
size(l2, 17); color(l2, cyan); hidden(l2);
// ---- 3. autoparallel: push it along its normals and it is itself, turned -------
circle(pspool, (812.0, 782.0), 25.0);
outlined(pspool); outline(pspool, mint); stroke(pspool, 2); opacity(pspool, 0.7);
param(pp0, (812.0, 782.0), 25.0000, 25.0000, "cos(t + 0.000000) + t*sin(t + 0.000000)", "sin(t + 0.000000) - t*cos(t + 0.000000)", (0, 5.6));
color(pp0, gold); stroke(pp0, 3); untraced(pp0);
param(pp1, (812.0, 782.0), 25.0000, 25.0000, "cos(t + 1.047198) + t*sin(t + 1.047198)", "sin(t + 1.047198) - t*cos(t + 1.047198)", (0, 5.6));
color(pp1, #6ba8f0); stroke(pp1, 3); untraced(pp1);
param(pp2, (812.0, 782.0), 25.0000, 25.0000, "cos(t + 2.094395) + t*sin(t + 2.094395)", "sin(t + 2.094395) - t*cos(t + 2.094395)", (0, 5.6));
color(pp2, #6ba8f0); stroke(pp2, 3); untraced(pp2);
param(pp3, (812.0, 782.0), 25.0000, 25.0000, "cos(t + 3.141593) + t*sin(t + 3.141593)", "sin(t + 3.141593) - t*cos(t + 3.141593)", (0, 5.6));
color(pp3, coral); stroke(pp3, 3); untraced(pp3);
param(pp4, (812.0, 782.0), 25.0000, 25.0000, "cos(t + 4.188790) + t*sin(t + 4.188790)", "sin(t + 4.188790) - t*cos(t + 4.188790)", (0, 5.6));
color(pp4, #6ba8f0); stroke(pp4, 3); untraced(pp4);
param(pp5, (812.0, 782.0), 25.0000, 25.0000, "cos(t + 5.235988) + t*sin(t + 5.235988)", "sin(t + 5.235988) - t*cos(t + 5.235988)", (0, 5.6));
color(pp5, #6ba8f0); stroke(pp5, 3); untraced(pp5);
text(l3, (812, 924), "six copies, a sixth of a turn apart");
size(l3, 16); color(l3, fg); opacity(l3, 0.75); hidden(l3);
equation(e3, (812, 958), `d=2\pi a\Rightarrow\text{itself}`, 18); color(e3, gold); hidden(e3);
// ---- 4. gears: the contact runs down a fixed line, at a ratio C cannot change ---
// pair 0: C = 3.0a, pressure angle 29.93°, contact runs 6.9 -> 63.3 px
circle(gb0a, (178.0, 1236.0), 46.00);
outlined(gb0a); outline(gb0a, dim); stroke(gb0a, 2); opacity(gb0a, 0.8);
circle(gb0b, (316.0, 1236.0), 73.60);
outlined(gb0b); outline(gb0b, dim); stroke(gb0b, 2); opacity(gb0b, 0.8);
line(gl0, (210.31, 1272.08), (259.77, 1186.16));
color(gl0, coral); stroke(gl0, 3); untraced(gl0);
circle(gp0, (231.08, 1236.00), 5); color(gp0, magenta); hidden(gp0);
cloud(gw0a, 440, #ffffff, 1.0) {
let m = 88;
let k = (i - mod(i, m))/m;
let p = mod(i, m)/m*1.45;
let s = 6.8846 + 56.4541*min(max((t - 13.08)/4.5, 0), 1);
let a = -0.522315 + s/46.0000 + k*1.256637;
let x = 178.0 + 46.0000*(cos(a - p) - p*sin(a - p));
let y = 1236.0 - 46.0000*(sin(a - p) + p*cos(a - p));
let hue = 42;
let sat = 0.8;
let r = 2.0;
}
cloud(gw0b, 704, #ffffff, 1.0) {
let m = 88;
let k = (i - mod(i, m))/m;
let p = mod(i, m)/m*1.085;
let s = 68.8465 - 6.8846 - 56.4541*min(max((t - 13.08)/4.5, 0), 1);
let a = 2.619278 + s/73.6000 + k*0.785398;
let x = 316.0 + 73.6000*(cos(a - p) - p*sin(a - p));
let y = 1236.0 - 73.6000*(sin(a - p) + p*cos(a - p));
let hue = 190;
let sat = 0.8;
let r = 2.0;
}
circle(gd0, (221.30, 1252.98), 6); color(gd0, magenta); hidden(gd0);
text(gt0, (247.0, 1386), "C = 3.0a pressure angle 29.93°");
size(gt0, 15); color(gt0, fg); opacity(gt0, 0.7); hidden(gt0);
text(gr0, (247.0, 1412), "ratio 0.625000000");
size(gr0, 16); color(gr0, magenta); hidden(gr0);
// pair 1: C = 3.6a, pressure angle 43.76°, contact runs 11.5 -> 105.4 px
circle(gb1a, (648.0, 1236.0), 46.00);
outlined(gb1a); outline(gb1a, dim); stroke(gb1a, 2); opacity(gb1a, 0.8);
circle(gb1b, (813.6, 1236.0), 73.60);
outlined(gb1b); outline(gb1b, dim); stroke(gb1b, 2); opacity(gb1b, 0.8);
line(gl1, (663.79, 1286.02), (777.87, 1166.89));
color(gl1, coral); stroke(gl1, 3); untraced(gl1);
circle(gp1, (711.69, 1236.00), 5); color(gp1, magenta); hidden(gp1);
cloud(gw1a, 440, #ffffff, 1.0) {
let m = 88;
let k = (i - mod(i, m))/m;
let p = mod(i, m)/m*1.45;
let s = 11.4539 + 93.9220*min(max((t - 13.08)/4.5, 0), 1);
let a = -0.763786 + s/46.0000 + k*1.256637;
let x = 648.0 + 46.0000*(cos(a - p) - p*sin(a - p));
let y = 1236.0 - 46.0000*(sin(a - p) + p*cos(a - p));
let hue = 42;
let sat = 0.8;
let r = 2.0;
}
cloud(gw1b, 704, #ffffff, 1.0) {
let m = 88;
let k = (i - mod(i, m))/m;
let p = mod(i, m)/m*1.085;
let s = 114.5391 - 11.4539 - 93.9220*min(max((t - 13.08)/4.5, 0), 1);
let a = 2.377806 + s/73.6000 + k*0.785398;
let x = 813.6 + 73.6000*(cos(a - p) - p*sin(a - p));
let y = 1236.0 - 73.6000*(sin(a - p) + p*cos(a - p));
let hue = 190;
let sat = 0.8;
let r = 2.0;
}
circle(gd1, (689.14, 1259.54), 6); color(gd1, magenta); hidden(gd1);
text(gt1, (730.8, 1386), "C = 3.6a pressure angle 43.76°");
size(gt1, 15); color(gt1, fg); opacity(gt1, 0.7); hidden(gt1);
text(gr1, (730.8, 1412), "ratio 0.625000000");
size(gr1, 16); color(gr1, magenta); hidden(gr1);
text(l4, (540, 1078), "5 teeth against 8 — the contact never leaves the coral line");
size(l4, 16); color(l4, coral); hidden(l4);
// ---- 5. the scroll compressor: the involute and its half-turn copy -------------
param(scfix, (268.0, 1604.0), 15.0000, 15.0000, "cos(t) + t*sin(t)", "sin(t) - t*cos(t)", (0.4, 7.6));
color(scfix, gold); stroke(scfix, 4); untraced(scfix);
param(scorb, (315.12, 1604.00), 15.0000, 15.0000, "cos(t + 3.141593) + t*sin(t + 3.141593)", "sin(t + 3.141593) - t*cos(t + 3.141593)", (0.4, 7.6));
color(scorb, violet); stroke(scorb, 4); untraced(scorb);
text(l5, (268, 1748), "orbit radius πa — the half-turn copy is the parallel at πa");
size(l5, 14); color(l5, violet); hidden(l5);
text(l5b, (268, 1774), "Léon Creux, 1905");
size(l5b, 13); color(l5b, fg); opacity(l5b, 0.6); hidden(l5b);
// ---- 6. far out it becomes a spiral of Archimedes -------------------------------
param(aarch, (812.0, 1596.0), 13.5000, 13.5000, "(t + 1.570796)*cos(t)", "(t + 1.570796)*sin(t)", (0, 12.0031));
color(aarch, coral); stroke(aarch, 3); dashed(aarch); opacity(aarch, 0.85); untraced(aarch);
param(ainv, (812.0, 1596.0), 13.5000, 13.5000, "cos(t) + t*sin(t)", "sin(t) - t*cos(t)", (0, 13.5));
color(ainv, gold); stroke(ainv, 3); untraced(ainv);
equation(e6, (812, 1738), `\rho\to a(\varphi+\tfrac{\pi}{2})`, 19); color(e6, coral); hidden(e6);
text(l6, (812, 1774), "the gap closes like −a/2t; pitch 2πa");
size(l6, 13); color(l6, fg); opacity(l6, 0.66); hidden(l6);
equation(foot, (540, 1866), `R^2=2as,\qquad R=at`, 24); color(foot, fg); hidden(foot);
seq {
// 1 — unwind the thread
par {
draw(dinv, 5.0);
seq { wait(1.2); show(l1a, 0.4); show(e1a, 0.4); show(e1b, 0.4); }
seq { wait(2.6); show(l1b, 0.4); }
seq { wait(3.6); show(e1c, 0.4); show(l1c, 0.4); }
}
// 2 — the normals, and 3 — the parallels
par {
draw(ninv, 1.8);
seq {
draw(nr0, 0.14);
draw(nr1, 0.14);
draw(nr2, 0.14);
draw(nr3, 0.14);
draw(nr4, 0.14);
draw(nr5, 0.14);
draw(nr6, 0.14);
draw(nr7, 0.14);
draw(nr8, 0.14);
draw(nr9, 0.14);
draw(nr10, 0.14);
draw(nr11, 0.14);
draw(nr12, 0.14);
draw(nr13, 0.14);
draw(nr14, 0.14);
draw(nr15, 0.14);
draw(nr16, 0.14);
draw(nr17, 0.14);
draw(nr18, 0.14);
draw(nr19, 0.14);
draw(nr20, 0.14);
draw(nr21, 0.14);
}
seq { wait(2.6); show(l2, 0.4); }
}
par {
seq { wait(0.00); draw(pp0, 1.5); }
seq { wait(0.28); draw(pp1, 1.5); }
seq { wait(0.56); draw(pp2, 1.5); }
seq { wait(0.84); draw(pp3, 1.5); }
seq { wait(1.12); draw(pp4, 1.5); }
seq { wait(1.40); draw(pp5, 1.5); }
seq { wait(2.2); show(l3, 0.4); show(e3, 0.4); }
}
// 4 — the gears
par {
draw(gl0, 1.1);
seq { wait(1.2); show(gp0, 0.3); show(gt0, 0.3); }
draw(gl1, 1.1);
seq { wait(1.2); show(gp1, 0.3); show(gt1, 0.3); }
seq { wait(1.6); show(l4, 0.4); }
}
par {
show(gd0, 0.2);
move(gd0, (249.47, 1204.06), 4.5, linear);
seq { wait(2.4); show(gr0, 0.4); }
show(gd1, 0.2);
move(gd1, (754.11, 1191.71), 4.5, linear);
seq { wait(2.4); show(gr1, 0.4); }
}
// 5 — the scroll pump orbits, 6 — the spiral limit
par {
draw(scfix, 2.0);
draw(scorb, 2.0);
seq { wait(1.4); draw(aarch, 1.8); }
draw(ainv, 2.6);
seq { wait(2.4); show(l5, 0.4); show(l5b, 0.4); show(e6, 0.4); show(l6, 0.4); }
}
par {
seq {
shift(scorb, (-1.606, -12.197), 0.200, linear);
shift(scorb, (-4.708, -11.365), 0.200, linear);
shift(scorb, (-7.489, -9.760), 0.200, linear);
shift(scorb, (-9.760, -7.489), 0.200, linear);
shift(scorb, (-11.365, -4.708), 0.200, linear);
shift(scorb, (-12.197, -1.606), 0.200, linear);
shift(scorb, (-12.197, 1.606), 0.200, linear);
shift(scorb, (-11.365, 4.708), 0.200, linear);
shift(scorb, (-9.760, 7.489), 0.200, linear);
shift(scorb, (-7.489, 9.760), 0.200, linear);
shift(scorb, (-4.708, 11.365), 0.200, linear);
shift(scorb, (-1.606, 12.197), 0.200, linear);
shift(scorb, (1.606, 12.197), 0.200, linear);
shift(scorb, (4.708, 11.365), 0.200, linear);
shift(scorb, (7.489, 9.760), 0.200, linear);
shift(scorb, (9.760, 7.489), 0.200, linear);
shift(scorb, (11.365, 4.708), 0.200, linear);
shift(scorb, (12.197, 1.606), 0.200, linear);
shift(scorb, (12.197, -1.606), 0.200, linear);
shift(scorb, (11.365, -4.708), 0.200, linear);
shift(scorb, (9.760, -7.489), 0.200, linear);
shift(scorb, (7.489, -9.760), 0.200, linear);
shift(scorb, (4.708, -11.365), 0.200, linear);
shift(scorb, (1.606, -12.197), 0.200, linear);
}
seq { wait(1.0); show(foot, 0.5); }
}
}
wait(2.6);
zoo-polar-complete
THE WHOLE POLAR TABLE ON ONE PLATE — 52 equations, 52 curves, in the order the table gives them, coloured by family. Nothing is hand-placed: every cell is fitted by the same procedure — sample ρ(θ) over its natural period, drop what is undefined or past a clip radius, cut the rest into contiguous arcs, scale their bounding box to the cell. 141 arcs over 52 cells; a curve with eight of them, like the MALTESE CROSS, is one the poles of its own formula have cut into eight pieces. The clip radius is chosen by squareness, because where you cut an unbounded curve decides what the cell shows — too near and the interesting part is a smear, too far and the asymptotic tails take the whole box. THE FIT FOUND A DUPLICATE ON ITS OWN: the table names two different curves TORPILLE, ρ = sin 4θ/sin θ and ρ = cos 2θ·cos θ. They are one curve, because sin 4θ = 4 sin θ cos θ cos 2θ exactly (1.3e-15) — and the autofit, which knows nothing of that identity, scaled them 23.111 and 92.444, a ratio of 4.0000. ρ² = cos 3θ appears twice as KIEPERT’S CURVE, so it is drawn once: 52 distinct equations out of 54 rows. READ ACROSS A PAIR AND YOU ARE READING AN INVERSION — ρ = f and ρ = 1/f are inverse curves in the unit circle, and the table is built almost entirely out of that one operation: circle/line, double egg/kampyle, folium/duplicating cubic, quadrifolium/cruciform, lemniscate/hyperbola, cardioid/parabola, Cayley/Tschirnhausen, cochleoid/quadratrix, Archimedes/hyperbolic spiral, Fermat/lituus. Ten pairs, one rule. The spirals are where the rule stops being about angles at all: ρ = θ and ρ = 1/θ invert to each other exactly as ρ = cos θ and ρ = sec θ do, but the curve never closes.
// zoo-polar-complete — the whole polar table on one plate. 52 equations, 52 curves,
// in the order the table gives them, with the family shown by colour.
//
// 1 powers of cos and their inverses 7 third angle
// 2 sin and cos mixed 8 cos 2θ over powers of cos θ
// 3 tangents 9 ratios of sines
// 4 cos 2θ 10 θ against sin θ
// 5 cos 3θ 11 spirals
// 6 half angle
//
// Nothing here is hand-placed. Every cell is fitted by the same procedure: sample ρ(θ)
// over its natural period, drop the samples where ρ is undefined or past a clip radius
// set from the 88th percentile of |ρ|, cut what survives into contiguous arcs, and
// scale the bounding box of those arcs to the cell. 141 arcs over 52 cells — a curve
// with eight of them, like the Maltese cross, is one that the poles of its own formula
// have cut into eight pieces.
//
// THE FIT FOUND A DUPLICATE ON ITS OWN. The table names two different curves TORPILLE:
// ρ = sin 4θ/sin θ and ρ = cos 2θ·cos θ. They are the same curve, because
// sin 4θ = 4 sin θ cos θ cos 2θ exactly (1.3e-15), so the first is four times the
// second. The autofit, which knows nothing of that identity, scaled them by 23.111 and
// 92.444 — a ratio of 4.0000. Two cells, one curve, and the arithmetic says so twice.
//
// ρ² = cos 3θ appears twice in the table as well, both times as KIEPERT'S CURVE, so it
// is drawn once. That leaves 52 distinct equations out of the 54 rows.
//
// READ ACROSS A PAIR AND YOU ARE READING AN INVERSION. ρ = f and ρ = 1/f are inverse
// curves in the unit circle, and the table is built almost entirely out of that one
// operation: circle/line, double egg/kampyle, folium/duplicating cubic, quadrifolium/
// cruciform, lemniscate/hyperbola, cardioid/parabola, Cayley/Tschirnhausen, cochleoid/
// quadratrix, Archimedes/hyperbolic spiral, Fermat/lituus. Ten pairs, one rule.
//
// The spirals are the row where the rule stops being about angles at all — ρ = θ and
// ρ = 1/θ invert to each other exactly as ρ = cos θ and ρ = sec θ do, but the curve
// never closes. Same operation, and the only family here that runs off the page.
//
// manic examples/zoo-polar-complete.manic
title("The whole polar table, on one plate");
canvas("9:16");
template("black");
bloom(0.18, 0.6, 16);
text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);
caption(head, "52 equations, one operation", (540, 108), 26);
text(sub, (540, 152), "ρ = f and ρ = 1/f are inverse curves — the table is built out of that");
size(sub, 17); color(sub, fg); opacity(sub, 0.66);
param(cer0, (38.00, 262.00), 104.0000, 104.0000,
"(cos(t))*cos(t)", "(cos(t))*sin(t)", (0.000000, 3.141593));
color(cer0, #f5c14b); stroke(cer0, 3); untraced(cer0);
equation(q_cer, (90.00, 334.00), `\rho=\cos\theta`, 16);
color(q_cer, #f5c14b); hidden(q_cer);
text(n_cer, (90.00, 352.00), "cercle");
size(n_cer, 11); color(n_cer, fg); hidden(n_cer);
param(dro0, (261.03, 262.00), 8.9688, 8.9688,
"(1/cos(t))*cos(t)", "(1/cos(t))*sin(t)", (-1.400000, 1.400000));
color(dro0, #f5c14b); stroke(dro0, 3); untraced(dro0);
equation(q_dro, (270.00, 334.00), `\rho=\sec\theta`, 16);
color(q_dro, #f5c14b); hidden(q_dro);
text(n_dro, (270.00, 352.00), "droite");
size(n_dro, 11); color(n_dro, fg); hidden(n_dro);
param(con0, (481.20, 262.00), 33.2800, 33.2800,
"(1/(1 + 0.6*cos(t)))*cos(t)", "(1/(1 + 0.6*cos(t)))*sin(t)", (0.000000, 6.283185));
color(con0, #f5c14b); stroke(con0, 3); untraced(con0);
equation(q_con, (450.00, 334.00), `\rho=p/(1+e\cos\theta)`, 16);
color(q_con, #f5c14b); hidden(q_con);
text(n_con, (450.00, 352.00), "conique");
size(n_con, 11); color(n_con, fg); hidden(n_con);
param(oeu0, (630.00, 262.00), 52.0000, 52.0000,
"(cos(t)*cos(t))*cos(t)", "(cos(t)*cos(t))*sin(t)", (0.000000, 6.283185));
color(oeu0, #f5c14b); stroke(oeu0, 3); untraced(oeu0);
equation(q_oeu, (630.00, 334.00), `\rho=\cos^2\theta`, 16);
color(q_oeu, #f5c14b); hidden(q_oeu);
text(n_oeu, (630.00, 352.00), "œuf double");
size(n_oeu, 11); color(n_oeu, fg); hidden(n_oeu);
param(kam0, (793.50, 262.00), 9.5998, 9.5998,
"(1/(cos(t)*cos(t)))*cos(t)", "(1/(cos(t)*cos(t)))*sin(t)", (-1.148000, 1.148000));
color(kam0, #f5c14b); stroke(kam0, 3); untraced(kam0);
equation(q_kam, (810.00, 334.00), `\rho=\sec^2\theta`, 16);
color(q_kam, #f5c14b); hidden(q_kam);
text(n_kam, (810.00, 352.00), "kampyle");
size(n_kam, 11); color(n_kam, fg); hidden(n_kam);
param(dip0, (990.00, 262.00), 52.0000, 52.0000,
"(sqrt(cos(t)))*cos(t)", "(sqrt(cos(t)))*sin(t)", (0.000000, 1.570796));
color(dip0, #f5c14b); stroke(dip0, 3); untraced(dip0);
param(dip1, (990.00, 262.00), 52.0000, 52.0000,
"(sqrt(cos(t)))*cos(t)", "(sqrt(cos(t)))*sin(t)", (4.713960, 6.283185));
color(dip1, #f5c14b); stroke(dip1, 3); untraced(dip1);
param(dip2, (990.00, 262.00), 52.0000, 52.0000,
"((0 - sqrt(cos(t))))*cos(t)", "((0 - sqrt(cos(t))))*sin(t)", (0.000000, 1.570796));
color(dip2, #f5c14b); stroke(dip2, 3); untraced(dip2);
param(dip3, (990.00, 262.00), 52.0000, 52.0000,
"((0 - sqrt(cos(t))))*cos(t)", "((0 - sqrt(cos(t))))*sin(t)", (4.713960, 6.283185));
color(dip3, #f5c14b); stroke(dip3, 3); untraced(dip3);
equation(q_dip, (990.00, 334.00), `\rho^2=\cos\theta`, 16);
color(q_dip, #f5c14b); hidden(q_dip);
text(n_dip, (990.00, 352.00), "courbe du dipôle");
size(n_dip, 11); color(n_dip, fg); hidden(n_dip);
param(kul0, (90.00, 432.00), 45.0821, 45.0821,
"(sqrt(1/cos(t)))*cos(t)", "(sqrt(1/cos(t)))*sin(t)", (0.000000, 1.003739));
color(kul0, #f5c14b); stroke(kul0, 3); untraced(kul0);
param(kul1, (90.00, 432.00), 45.0821, 45.0821,
"(sqrt(1/cos(t)))*cos(t)", "(sqrt(1/cos(t)))*sin(t)", (5.277876, 6.283185));
color(kul1, #f5c14b); stroke(kul1, 3); untraced(kul1);
param(kul2, (90.00, 432.00), 45.0821, 45.0821,
"((0 - sqrt(1/cos(t))))*cos(t)", "((0 - sqrt(1/cos(t))))*sin(t)", (0.000000, 1.003739));
color(kul2, #f5c14b); stroke(kul2, 3); untraced(kul2);
param(kul3, (90.00, 432.00), 45.0821, 45.0821,
"((0 - sqrt(1/cos(t))))*cos(t)", "((0 - sqrt(1/cos(t))))*sin(t)", (5.277876, 6.283185));
color(kul3, #f5c14b); stroke(kul3, 3); untraced(kul3);
equation(q_kul, (90.00, 504.00), `\rho^2=\sec\theta`, 16);
color(q_kul, #f5c14b); hidden(q_kul);
text(n_kul, (90.00, 522.00), "cf. quartique de Külp");
size(n_kul, 11); color(n_kul, fg); hidden(n_kul);
param(fos0, (218.00, 432.00), 104.0000, 104.0000,
"(cos(t)*cos(t)*cos(t))*cos(t)", "(cos(t)*cos(t)*cos(t))*sin(t)", (0.000000, 6.283185));
color(fos0, #f5c14b); stroke(fos0, 3); untraced(fos0);
equation(q_fos, (270.00, 504.00), `\rho=\cos^3\theta`, 16);
color(q_fos, #f5c14b); hidden(q_fos);
text(n_fos, (270.00, 522.00), "folium simple");
size(n_fos, 11); color(n_fos, fg); hidden(n_fos);
param(dup0, (431.00, 432.00), 7.6890, 7.6890,
"(1/(cos(t)*cos(t)*cos(t)))*cos(t)", "(1/(cos(t)*cos(t)*cos(t)))*sin(t)", (-1.043000, 1.043000));
color(dup0, #f5c14b); stroke(dup0, 3); untraced(dup0);
equation(q_dup, (450.00, 504.00), `\rho=\sec^3\theta`, 16);
color(q_dup, #f5c14b); hidden(q_dup);
text(n_dup, (450.00, 522.00), "cubique duplicatrice");
size(n_dup, 11); color(n_dup, fg); hidden(n_dup);
param(agn0, (617.34, 432.19), 21.6283, 21.6283,
"(sign(1/cos(t))*exp(ln(abs(1/cos(t)) + 0.000000001)/3))*cos(t)", "(sign(1/cos(t))*exp(ln(abs(1/cos(t)) + 0.000000001)/3))*sin(t)", (0.000000, 1.500110));
color(agn0, #f5c14b); stroke(agn0, 3); untraced(agn0);
param(agn1, (617.34, 432.19), 21.6283, 21.6283,
"(sign(1/cos(t))*exp(ln(abs(1/cos(t)) + 0.000000001)/3))*cos(t)", "(sign(1/cos(t))*exp(ln(abs(1/cos(t)) + 0.000000001)/3))*sin(t)", (1.643053, 4.641703));
color(agn1, #f5c14b); stroke(agn1, 3); untraced(agn1);
param(agn2, (617.34, 432.19), 21.6283, 21.6283,
"(sign(1/cos(t))*exp(ln(abs(1/cos(t)) + 0.000000001)/3))*cos(t)", "(sign(1/cos(t))*exp(ln(abs(1/cos(t)) + 0.000000001)/3))*sin(t)", (4.784646, 6.283185));
color(agn2, #f5c14b); stroke(agn2, 3); untraced(agn2);
equation(q_agn, (630.00, 504.00), `\rho^3=\sec\theta`, 16);
color(q_agn, #f5c14b); hidden(q_agn);
text(n_agn, (630.00, 522.00), "cf. cubique d'Agnesi");
size(n_agn, 11); color(n_agn, fg); hidden(n_agn);
param(par0, (758.00, 432.00), 21.2023, 21.2023,
"(cos(t)/(sin(t)*sin(t)))*cos(t)", "(cos(t)/(sin(t)*sin(t)))*sin(t)", (0.424115, 2.715907));
color(par0, #4fd6e8); stroke(par0, 3); untraced(par0);
param(par1, (758.00, 432.00), 21.2023, 21.2023,
"(cos(t)/(sin(t)*sin(t)))*cos(t)", "(cos(t)/(sin(t)*sin(t)))*sin(t)", (3.567278, 5.859070));
color(par1, #4fd6e8); stroke(par1, 3); untraced(par1);
equation(q_par, (810.00, 504.00), `\rho=\cos\theta/\sin^2\theta`, 16);
color(q_par, #4fd6e8); hidden(q_par);
text(n_par, (810.00, 522.00), "parabole");
size(n_par, 11); color(n_par, fg); hidden(n_par);
param(cis0, (957.55, 432.00), 166.0320, 166.0320,
"((sin(t)*sin(t))/cos(t))*cos(t)", "((sin(t)*sin(t))/cos(t))*sin(t)", (0.000000, 0.675442));
color(cis0, #4fd6e8); stroke(cis0, 3); untraced(cis0);
param(cis1, (957.55, 432.00), 166.0320, 166.0320,
"((sin(t)*sin(t))/cos(t))*cos(t)", "((sin(t)*sin(t))/cos(t))*sin(t)", (2.466150, 3.817035));
color(cis1, #4fd6e8); stroke(cis1, 3); untraced(cis1);
param(cis2, (957.55, 432.00), 166.0320, 166.0320,
"((sin(t)*sin(t))/cos(t))*cos(t)", "((sin(t)*sin(t))/cos(t))*sin(t)", (5.609314, 6.283185));
color(cis2, #4fd6e8); stroke(cis2, 3); untraced(cis2);
equation(q_cis, (990.00, 504.00), `\rho=\sin^2\theta/\cos\theta`, 16);
color(q_cis, #4fd6e8); hidden(q_cis);
text(n_cis, (990.00, 522.00), "cissoïde droite");
size(n_cis, 11); color(n_cis, fg); hidden(n_cis);
param(bif0, (69.99, 602.00), 160.1185, 160.1185,
"(cos(t)*sin(t)*sin(t))*cos(t)", "(cos(t)*sin(t)*sin(t))*sin(t)", (0.000000, 6.283185));
color(bif0, #4fd6e8); stroke(bif0, 3); untraced(bif0);
equation(q_bif, (90.00, 674.00), `\rho=\cos\theta\sin^2\theta`, 16);
color(q_bif, #4fd6e8); hidden(q_bif);
text(n_bif, (90.00, 692.00), "bifolium");
size(n_bif, 11); color(n_bif, fg); hidden(n_bif);
param(mix0, (217.94, 602.00), 0.0578, 0.0578,
"(1/(cos(t)*sin(t)*sin(t)))*cos(t)", "(1/(cos(t)*sin(t)*sin(t)))*sin(t)", (0.023562, 1.569226));
color(mix0, #4fd6e8); stroke(mix0, 3); untraced(mix0);
param(mix1, (217.94, 602.00), 0.0578, 0.0578,
"(1/(cos(t)*sin(t)*sin(t)))*cos(t)", "(1/(cos(t)*sin(t)*sin(t)))*sin(t)", (1.572367, 3.118031));
color(mix1, #4fd6e8); stroke(mix1, 3); untraced(mix1);
param(mix2, (217.94, 602.00), 0.0578, 0.0578,
"(1/(cos(t)*sin(t)*sin(t)))*cos(t)", "(1/(cos(t)*sin(t)*sin(t)))*sin(t)", (3.165155, 4.710818));
color(mix2, #4fd6e8); stroke(mix2, 3); untraced(mix2);
param(mix3, (217.94, 602.00), 0.0578, 0.0578,
"(1/(cos(t)*sin(t)*sin(t)))*cos(t)", "(1/(cos(t)*sin(t)*sin(t)))*sin(t)", (4.713960, 6.259623));
color(mix3, #4fd6e8); stroke(mix3, 3); untraced(mix3);
equation(q_mix, (270.00, 674.00), `\rho=1/(\cos\theta\sin^2\theta)`, 16);
color(q_mix, #4fd6e8); hidden(q_mix);
text(n_mix, (270.00, 692.00), "cubique mixte");
size(n_mix, 11); color(n_mix, fg); hidden(n_mix);
param(sem0, (417.55, 602.00), 101.1254, 101.1254,
"((sin(t)*sin(t))/(cos(t)*cos(t)*cos(t)))*cos(t)", "((sin(t)*sin(t))/(cos(t)*cos(t)*cos(t)))*sin(t)", (0.000000, 0.675442));
color(sem0, #4fd6e8); stroke(sem0, 3); untraced(sem0);
param(sem1, (417.55, 602.00), 101.1254, 101.1254,
"((sin(t)*sin(t))/(cos(t)*cos(t)*cos(t)))*cos(t)", "((sin(t)*sin(t))/(cos(t)*cos(t)*cos(t)))*sin(t)", (2.466150, 3.817035));
color(sem1, #4fd6e8); stroke(sem1, 3); untraced(sem1);
param(sem2, (417.55, 602.00), 101.1254, 101.1254,
"((sin(t)*sin(t))/(cos(t)*cos(t)*cos(t)))*cos(t)", "((sin(t)*sin(t))/(cos(t)*cos(t)*cos(t)))*sin(t)", (5.609314, 6.283185));
color(sem2, #4fd6e8); stroke(sem2, 3); untraced(sem2);
equation(q_sem, (450.00, 674.00), `\rho=\sin^2\theta/\cos^3\theta`, 16);
color(q_sem, #4fd6e8); hidden(q_sem);
text(n_sem, (450.00, 692.00), "parabole semi-cubique");
size(n_sem, 11); color(n_sem, fg); hidden(n_sem);
param(kap0, (630.00, 602.00), 62.5258, 62.5258,
"(tan(t))*cos(t)", "(tan(t))*sin(t)", (0.000000, 0.838805));
color(kap0, #5ee6a8); stroke(kap0, 3); untraced(kap0);
param(kap1, (630.00, 602.00), 62.5258, 62.5258,
"(tan(t))*cos(t)", "(tan(t))*sin(t)", (2.301217, 3.980398));
color(kap1, #5ee6a8); stroke(kap1, 3); untraced(kap1);
param(kap2, (630.00, 602.00), 62.5258, 62.5258,
"(tan(t))*cos(t)", "(tan(t))*sin(t)", (5.442809, 6.283185));
color(kap2, #5ee6a8); stroke(kap2, 3); untraced(kap2);
equation(q_kap, (630.00, 674.00), `\rho=\tan\theta`, 16);
color(q_kap, #5ee6a8); hidden(q_kap);
text(n_kap, (630.00, 692.00), "kappa");
size(n_kap, 11); color(n_kap, fg); hidden(n_kap);
param(mou0, (810.00, 602.00), 1.8258, 1.8258,
"(tan(2*t))*cos(t)", "(tan(2*t))*sin(t)", (0.000000, 0.772832));
color(mou0, #5ee6a8); stroke(mou0, 3); untraced(mou0);
param(mou1, (810.00, 602.00), 1.8258, 1.8258,
"(tan(2*t))*cos(t)", "(tan(2*t))*sin(t)", (0.797965, 2.343628));
color(mou1, #5ee6a8); stroke(mou1, 3); untraced(mou1);
param(mou2, (810.00, 602.00), 1.8258, 1.8258,
"(tan(2*t))*cos(t)", "(tan(2*t))*sin(t)", (2.368761, 3.914424));
color(mou2, #5ee6a8); stroke(mou2, 3); untraced(mou2);
param(mou3, (810.00, 602.00), 1.8258, 1.8258,
"(tan(2*t))*cos(t)", "(tan(2*t))*sin(t)", (3.939557, 5.485221));
color(mou3, #5ee6a8); stroke(mou3, 3); untraced(mou3);
param(mou4, (810.00, 602.00), 1.8258, 1.8258,
"(tan(2*t))*cos(t)", "(tan(2*t))*sin(t)", (5.510354, 6.283185));
color(mou4, #5ee6a8); stroke(mou4, 3); untraced(mou4);
equation(q_mou, (810.00, 674.00), `\rho=\tan 2\theta`, 16);
color(q_mou, #5ee6a8); hidden(q_mou);
text(n_mou, (810.00, 692.00), "moulin à vent");
size(n_mou, 11); color(n_mou, fg); hidden(n_mou);
param(ser0, (990.00, 602.00), 63.1986, 63.1986,
"(sqrt(tan(t)))*cos(t)", "(sqrt(tan(t)))*sin(t)", (0.000000, 0.863938));
color(ser0, #5ee6a8); stroke(ser0, 3); untraced(ser0);
param(ser1, (990.00, 602.00), 63.1986, 63.1986,
"(sqrt(tan(t)))*cos(t)", "(sqrt(tan(t)))*sin(t)", (3.143163, 4.005531));
color(ser1, #5ee6a8); stroke(ser1, 3); untraced(ser1);
param(ser2, (990.00, 602.00), 63.1986, 63.1986,
"((0 - sqrt(tan(t))))*cos(t)", "((0 - sqrt(tan(t))))*sin(t)", (0.000000, 0.863938));
color(ser2, #5ee6a8); stroke(ser2, 3); untraced(ser2);
param(ser3, (990.00, 602.00), 63.1986, 63.1986,
"((0 - sqrt(tan(t))))*cos(t)", "((0 - sqrt(tan(t))))*sin(t)", (3.143163, 4.005531));
color(ser3, #5ee6a8); stroke(ser3, 3); untraced(ser3);
equation(q_ser, (990.00, 674.00), `\rho^2=\tan\theta`, 16);
color(q_ser, #5ee6a8); hidden(q_ser);
text(n_ser, (990.00, 692.00), "serpentine droite");
size(n_ser, 11); color(n_ser, fg); hidden(n_ser);
param(swa0, (90.00, 772.00), 10.7883, 10.7883,
"(sqrt(tan(2*t)))*cos(t)", "(sqrt(tan(2*t)))*sin(t)", (0.000000, 0.774403));
color(swa0, #5ee6a8); stroke(swa0, 3); untraced(swa0);
param(swa1, (90.00, 772.00), 10.7883, 10.7883,
"(sqrt(tan(2*t)))*cos(t)", "(sqrt(tan(2*t)))*sin(t)", (1.572367, 2.343628));
color(swa1, #5ee6a8); stroke(swa1, 3); untraced(swa1);
param(swa2, (90.00, 772.00), 10.7883, 10.7883,
"(sqrt(tan(2*t)))*cos(t)", "(sqrt(tan(2*t)))*sin(t)", (3.143163, 3.915995));
color(swa2, #5ee6a8); stroke(swa2, 3); untraced(swa2);
param(swa3, (90.00, 772.00), 10.7883, 10.7883,
"(sqrt(tan(2*t)))*cos(t)", "(sqrt(tan(2*t)))*sin(t)", (4.713960, 5.485221));
color(swa3, #5ee6a8); stroke(swa3, 3); untraced(swa3);
param(swa4, (90.00, 772.00), 10.7883, 10.7883,
"((0 - sqrt(tan(2*t))))*cos(t)", "((0 - sqrt(tan(2*t))))*sin(t)", (0.000000, 0.774403));
color(swa4, #5ee6a8); stroke(swa4, 3); untraced(swa4);
param(swa5, (90.00, 772.00), 10.7883, 10.7883,
"((0 - sqrt(tan(2*t))))*cos(t)", "((0 - sqrt(tan(2*t))))*sin(t)", (1.572367, 2.343628));
color(swa5, #5ee6a8); stroke(swa5, 3); untraced(swa5);
param(swa6, (90.00, 772.00), 10.7883, 10.7883,
"((0 - sqrt(tan(2*t))))*cos(t)", "((0 - sqrt(tan(2*t))))*sin(t)", (3.143163, 3.915995));
color(swa6, #5ee6a8); stroke(swa6, 3); untraced(swa6);
param(swa7, (90.00, 772.00), 10.7883, 10.7883,
"((0 - sqrt(tan(2*t))))*cos(t)", "((0 - sqrt(tan(2*t))))*sin(t)", (4.713960, 5.485221));
color(swa7, #5ee6a8); stroke(swa7, 3); untraced(swa7);
equation(q_swa, (90.00, 844.00), `\rho^2=\tan 2\theta`, 16);
color(q_swa, #5ee6a8); hidden(q_swa);
text(n_swa, (90.00, 862.00), "swastika");
size(n_swa, 11); color(n_swa, fg); hidden(n_swa);
param(qua0, (270.00, 772.00), 52.0000, 52.0000,
"(cos(2*t))*cos(t)", "(cos(2*t))*sin(t)", (0.000000, 6.283185));
color(qua0, #f06ec8); stroke(qua0, 3); untraced(qua0);
equation(q_qua, (270.00, 844.00), `\rho=\cos 2\theta`, 16);
color(q_qua, #f06ec8); hidden(q_qua);
text(n_qua, (270.00, 862.00), "quadrifolium");
size(n_qua, 11); color(n_qua, fg); hidden(n_qua);
param(cru0, (450.00, 772.00), 1.8253, 1.8253,
"(1/cos(2*t))*cos(t)", "(1/cos(2*t))*sin(t)", (0.000000, 0.772832));
color(cru0, #f06ec8); stroke(cru0, 3); untraced(cru0);
param(cru1, (450.00, 772.00), 1.8253, 1.8253,
"(1/cos(2*t))*cos(t)", "(1/cos(2*t))*sin(t)", (0.797965, 2.343628));
color(cru1, #f06ec8); stroke(cru1, 3); untraced(cru1);
param(cru2, (450.00, 772.00), 1.8253, 1.8253,
"(1/cos(2*t))*cos(t)", "(1/cos(2*t))*sin(t)", (2.368761, 3.914424));
color(cru2, #f06ec8); stroke(cru2, 3); untraced(cru2);
param(cru3, (450.00, 772.00), 1.8253, 1.8253,
"(1/cos(2*t))*cos(t)", "(1/cos(2*t))*sin(t)", (3.939557, 5.485221));
color(cru3, #f06ec8); stroke(cru3, 3); untraced(cru3);
param(cru4, (450.00, 772.00), 1.8253, 1.8253,
"(1/cos(2*t))*cos(t)", "(1/cos(2*t))*sin(t)", (5.510354, 6.283185));
color(cru4, #f06ec8); stroke(cru4, 3); untraced(cru4);
equation(q_cru, (450.00, 844.00), `\rho=\sec 2\theta`, 16);
color(q_cru, #f06ec8); hidden(q_cru);
text(n_cru, (450.00, 862.00), "cruciforme");
size(n_cru, 11); color(n_cru, fg); hidden(n_cru);
param(lem0, (630.00, 772.00), 52.0000, 52.0000,
"(sqrt(cos(2*t)))*cos(t)", "(sqrt(cos(2*t)))*sin(t)", (0.000000, 0.785398));
color(lem0, #f06ec8); stroke(lem0, 3); untraced(lem0);
param(lem1, (630.00, 772.00), 52.0000, 52.0000,
"(sqrt(cos(2*t)))*cos(t)", "(sqrt(cos(2*t)))*sin(t)", (2.357765, 3.926991));
color(lem1, #f06ec8); stroke(lem1, 3); untraced(lem1);
param(lem2, (630.00, 772.00), 52.0000, 52.0000,
"(sqrt(cos(2*t)))*cos(t)", "(sqrt(cos(2*t)))*sin(t)", (5.499358, 6.283185));
color(lem2, #f06ec8); stroke(lem2, 3); untraced(lem2);
param(lem3, (630.00, 772.00), 52.0000, 52.0000,
"((0 - sqrt(cos(2*t))))*cos(t)", "((0 - sqrt(cos(2*t))))*sin(t)", (0.000000, 0.785398));
color(lem3, #f06ec8); stroke(lem3, 3); untraced(lem3);
param(lem4, (630.00, 772.00), 52.0000, 52.0000,
"((0 - sqrt(cos(2*t))))*cos(t)", "((0 - sqrt(cos(2*t))))*sin(t)", (2.357765, 3.926991));
color(lem4, #f06ec8); stroke(lem4, 3); untraced(lem4);
param(lem5, (630.00, 772.00), 52.0000, 52.0000,
"((0 - sqrt(cos(2*t))))*cos(t)", "((0 - sqrt(cos(2*t))))*sin(t)", (5.499358, 6.283185));
color(lem5, #f06ec8); stroke(lem5, 3); untraced(lem5);
equation(q_lem, (630.00, 844.00), `\rho^2=\cos 2\theta`, 16);
color(q_lem, #f06ec8); hidden(q_lem);
text(n_lem, (630.00, 862.00), "lemniscate de Bernoulli");
size(n_lem, 11); color(n_lem, fg); hidden(n_lem);
param(hy20, (810.00, 772.00), 11.5140, 11.5140,
"(sqrt(1/cos(2*t)))*cos(t)", "(sqrt(1/cos(2*t)))*sin(t)", (0.000000, 0.772832));
color(hy20, #f06ec8); stroke(hy20, 3); untraced(hy20);
param(hy21, (810.00, 772.00), 11.5140, 11.5140,
"(sqrt(1/cos(2*t)))*cos(t)", "(sqrt(1/cos(2*t)))*sin(t)", (2.368761, 3.914424));
color(hy21, #f06ec8); stroke(hy21, 3); untraced(hy21);
param(hy22, (810.00, 772.00), 11.5140, 11.5140,
"(sqrt(1/cos(2*t)))*cos(t)", "(sqrt(1/cos(2*t)))*sin(t)", (5.510354, 6.283185));
color(hy22, #f06ec8); stroke(hy22, 3); untraced(hy22);
param(hy23, (810.00, 772.00), 11.5140, 11.5140,
"((0 - sqrt(1/cos(2*t))))*cos(t)", "((0 - sqrt(1/cos(2*t))))*sin(t)", (0.000000, 0.772832));
color(hy23, #f06ec8); stroke(hy23, 3); untraced(hy23);
param(hy24, (810.00, 772.00), 11.5140, 11.5140,
"((0 - sqrt(1/cos(2*t))))*cos(t)", "((0 - sqrt(1/cos(2*t))))*sin(t)", (2.368761, 3.914424));
color(hy24, #f06ec8); stroke(hy24, 3); untraced(hy24);
param(hy25, (810.00, 772.00), 11.5140, 11.5140,
"((0 - sqrt(1/cos(2*t))))*cos(t)", "((0 - sqrt(1/cos(2*t))))*sin(t)", (5.510354, 6.283185));
color(hy25, #f06ec8); stroke(hy25, 3); untraced(hy25);
equation(q_hy2, (810.00, 844.00), `\rho^2=\sec 2\theta`, 16);
color(q_hy2, #f06ec8); hidden(q_hy2);
text(n_hy2, (810.00, 862.00), "hyperbole");
size(n_hy2, 11); color(n_hy2, fg); hidden(n_hy2);
param(mal0, (990.00, 772.00), 8.8995, 8.8995,
"(sqrt(1/cos(4*t)))*cos(t)", "(sqrt(1/cos(4*t)))*sin(t)", (0.000000, 0.386416));
color(mal0, #f06ec8); stroke(mal0, 3); untraced(mal0);
param(mal1, (990.00, 772.00), 8.8995, 8.8995,
"(sqrt(1/cos(4*t)))*cos(t)", "(sqrt(1/cos(4*t)))*sin(t)", (1.184380, 1.957212));
color(mal1, #f06ec8); stroke(mal1, 3); untraced(mal1);
param(mal2, (990.00, 772.00), 8.8995, 8.8995,
"(sqrt(1/cos(4*t)))*cos(t)", "(sqrt(1/cos(4*t)))*sin(t)", (2.755177, 3.528009));
color(mal2, #f06ec8); stroke(mal2, 3); untraced(mal2);
param(mal3, (990.00, 772.00), 8.8995, 8.8995,
"(sqrt(1/cos(4*t)))*cos(t)", "(sqrt(1/cos(4*t)))*sin(t)", (4.327544, 5.098805));
color(mal3, #f06ec8); stroke(mal3, 3); untraced(mal3);
param(mal4, (990.00, 772.00), 8.8995, 8.8995,
"(sqrt(1/cos(4*t)))*cos(t)", "(sqrt(1/cos(4*t)))*sin(t)", (5.898340, 6.283185));
color(mal4, #f06ec8); stroke(mal4, 3); untraced(mal4);
param(mal5, (990.00, 772.00), 8.8995, 8.8995,
"((0 - sqrt(1/cos(4*t))))*cos(t)", "((0 - sqrt(1/cos(4*t))))*sin(t)", (0.000000, 0.386416));
color(mal5, #f06ec8); stroke(mal5, 3); untraced(mal5);
param(mal6, (990.00, 772.00), 8.8995, 8.8995,
"((0 - sqrt(1/cos(4*t))))*cos(t)", "((0 - sqrt(1/cos(4*t))))*sin(t)", (1.184380, 1.957212));
color(mal6, #f06ec8); stroke(mal6, 3); untraced(mal6);
param(mal7, (990.00, 772.00), 8.8995, 8.8995,
"((0 - sqrt(1/cos(4*t))))*cos(t)", "((0 - sqrt(1/cos(4*t))))*sin(t)", (2.755177, 3.528009));
color(mal7, #f06ec8); stroke(mal7, 3); untraced(mal7);
param(mal8, (990.00, 772.00), 8.8995, 8.8995,
"((0 - sqrt(1/cos(4*t))))*cos(t)", "((0 - sqrt(1/cos(4*t))))*sin(t)", (4.327544, 5.098805));
color(mal8, #f06ec8); stroke(mal8, 3); untraced(mal8);
param(mal9, (990.00, 772.00), 8.8995, 8.8995,
"((0 - sqrt(1/cos(4*t))))*cos(t)", "((0 - sqrt(1/cos(4*t))))*sin(t)", (5.898340, 6.283185));
color(mal9, #f06ec8); stroke(mal9, 3); untraced(mal9);
equation(q_mal, (990.00, 844.00), `\rho^2=\sec 4\theta`, 16);
color(q_mal, #f06ec8); hidden(q_mal);
text(n_mal, (990.00, 862.00), "croix de Malte");
size(n_mal, 11); color(n_mal, fg); hidden(n_mal);
param(tri0, (77.07, 942.00), 59.0884, 59.0884,
"(cos(3*t))*cos(t)", "(cos(3*t))*sin(t)", (0.000000, 6.283185));
color(tri0, #a98cf0); stroke(tri0, 3); untraced(tri0);
equation(q_tri, (90.00, 1014.00), `\rho=\cos 3\theta`, 16);
color(q_tri, #a98cf0); hidden(q_tri);
text(n_tri, (90.00, 1032.00), "trifolium régulier");
size(n_tri, 11); color(n_tri, fg); hidden(n_tri);
param(tre0, (268.42, 937.27), 1.3365, 1.3365,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (0.000000, 0.515221));
color(tre0, #a98cf0); stroke(tre0, 3); untraced(tre0);
param(tre1, (268.42, 937.27), 1.3365, 1.3365,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (0.532500, 1.562942));
color(tre1, #a98cf0); stroke(tre1, 3); untraced(tre1);
param(tre2, (268.42, 937.27), 1.3365, 1.3365,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (1.580221, 2.609093));
color(tre2, #a98cf0); stroke(tre2, 3); untraced(tre2);
param(tre3, (268.42, 937.27), 1.3365, 1.3365,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (2.626371, 3.656814));
color(tre3, #a98cf0); stroke(tre3, 3); untraced(tre3);
param(tre4, (268.42, 937.27), 1.3365, 1.3365,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (3.674093, 4.704535));
color(tre4, #a98cf0); stroke(tre4, 3); untraced(tre4);
param(tre5, (268.42, 937.27), 1.3365, 1.3365,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (4.721814, 5.750685));
color(tre5, #a98cf0); stroke(tre5, 3); untraced(tre5);
param(tre6, (268.42, 937.27), 1.3365, 1.3365,
"(1/cos(3*t))*cos(t)", "(1/cos(3*t))*sin(t)", (5.767964, 6.283185));
color(tre6, #a98cf0); stroke(tre6, 3); untraced(tre6);
equation(q_tre, (270.00, 1014.00), `\rho=\sec 3\theta`, 16);
color(q_tre, #a98cf0); hidden(q_tre);
text(n_tre, (270.00, 1032.00), "trèfle équilatère");
size(n_tre, 11); color(n_tre, fg); hidden(n_tre);
param(kie0, (450.00, 942.00), 52.0000, 52.0000,
"(sqrt(cos(3*t)))*cos(t)", "(sqrt(cos(3*t)))*sin(t)", (0.000000, 0.523075));
color(kie0, #a98cf0); stroke(kie0, 3); untraced(kie0);
param(kie1, (450.00, 942.00), 52.0000, 52.0000,
"(sqrt(cos(3*t)))*cos(t)", "(sqrt(cos(3*t)))*sin(t)", (1.572367, 2.616947));
color(kie1, #a98cf0); stroke(kie1, 3); untraced(kie1);
param(kie2, (450.00, 942.00), 52.0000, 52.0000,
"(sqrt(cos(3*t)))*cos(t)", "(sqrt(cos(3*t)))*sin(t)", (3.666239, 4.712389));
color(kie2, #a98cf0); stroke(kie2, 3); untraced(kie2);
param(kie3, (450.00, 942.00), 52.0000, 52.0000,
"(sqrt(cos(3*t)))*cos(t)", "(sqrt(cos(3*t)))*sin(t)", (5.760110, 6.283185));
color(kie3, #a98cf0); stroke(kie3, 3); untraced(kie3);
param(kie4, (450.00, 942.00), 52.0000, 52.0000,
"((0 - sqrt(cos(3*t))))*cos(t)", "((0 - sqrt(cos(3*t))))*sin(t)", (0.000000, 0.523075));
color(kie4, #a98cf0); stroke(kie4, 3); untraced(kie4);
param(kie5, (450.00, 942.00), 52.0000, 52.0000,
"((0 - sqrt(cos(3*t))))*cos(t)", "((0 - sqrt(cos(3*t))))*sin(t)", (1.572367, 2.616947));
color(kie5, #a98cf0); stroke(kie5, 3); untraced(kie5);
param(kie6, (450.00, 942.00), 52.0000, 52.0000,
"((0 - sqrt(cos(3*t))))*cos(t)", "((0 - sqrt(cos(3*t))))*sin(t)", (3.666239, 4.712389));
color(kie6, #a98cf0); stroke(kie6, 3); untraced(kie6);
param(kie7, (450.00, 942.00), 52.0000, 52.0000,
"((0 - sqrt(cos(3*t))))*cos(t)", "((0 - sqrt(cos(3*t))))*sin(t)", (5.760110, 6.283185));
color(kie7, #a98cf0); stroke(kie7, 3); untraced(kie7);
equation(q_kie, (450.00, 1014.00), `\rho^2=\cos 3\theta`, 16);
color(q_kie, #a98cf0); hidden(q_kie);
text(n_kie, (450.00, 1032.00), "courbe de Kiepert");
size(n_kie, 11); color(n_kie, fg); hidden(n_kie);
param(dur0, (630.00, 942.00), 52.0000, 52.0000,
"(cos(t/2))*cos(t)", "(cos(t/2))*sin(t)", (0.000000, 12.566371));
color(dur0, #f0785a); stroke(dur0, 3); untraced(dur0);
equation(q_dur, (630.00, 1014.00), `\rho=\cos(\theta/2)`, 16);
color(q_dur, #f0785a); hidden(q_dur);
text(n_dur, (630.00, 1032.00), "folium de Dürer");
size(n_dur, 11); color(n_dur, fg); hidden(n_dur);
param(del0, (837.26, 942.00), 24.7422, 24.7422,
"(1/cos(t/2))*cos(t)", "(1/cos(t/2))*sin(t)", (-2.600000, 2.600000));
color(del0, #f0785a); stroke(del0, 3); untraced(del0);
equation(q_del, (810.00, 1014.00), `\rho=\sec(\theta/2)`, 16);
color(q_del, #f0785a); hidden(q_del);
text(n_del, (810.00, 1032.00), "trisectrice de Delange");
size(n_del, 11); color(n_del, fg); hidden(n_del);
param(car0, (954.97, 942.00), 80.0592, 80.0592,
"(cos(t/2)*cos(t/2))*cos(t)", "(cos(t/2)*cos(t/2))*sin(t)", (0.000000, 12.566371));
color(car0, #f0785a); stroke(car0, 3); untraced(car0);
equation(q_car, (990.00, 1014.00), `\rho=\cos^2(\theta/2)`, 16);
color(q_car, #f0785a); hidden(q_car);
text(n_car, (990.00, 1032.00), "cardioïde");
size(n_car, 11); color(n_car, fg); hidden(n_car);
param(pa20, (107.41, 1112.00), 11.6357, 11.6357,
"(1/(cos(t/2)*cos(t/2)))*cos(t)", "(1/(cos(t/2)*cos(t/2)))*sin(t)", (-2.300000, 2.300000));
color(pa20, #f0785a); stroke(pa20, 3); untraced(pa20);
equation(q_pa2, (90.00, 1184.00), `\rho=\sec^2(\theta/2)`, 16);
color(q_pa2, #f0785a); hidden(q_pa2);
text(n_pa2, (90.00, 1202.00), "parabole");
size(n_pa2, 11); color(n_pa2, fg); hidden(n_pa2);
param(lim0, (257.07, 1112.00), 59.0884, 59.0884,
"(cos(t/3))*cos(t)", "(cos(t/3))*sin(t)", (-4.712389, 4.712389));
color(lim0, #a8e05a); stroke(lim0, 3); untraced(lim0);
equation(q_lim, (270.00, 1184.00), `\rho=\cos(\theta/3)`, 16);
color(q_lim, #a8e05a); hidden(q_lim);
text(n_lim, (270.00, 1202.00), "limaçon trisecteur");
size(n_lim, 11); color(n_lim, fg); hidden(n_lim);
param(mac0, (459.55, 1112.00), 10.1406, 10.1406,
"(1/cos(t/3))*cos(t)", "(1/cos(t/3))*sin(t)", (-4.200000, 4.200000));
color(mac0, #a8e05a); stroke(mac0, 3); untraced(mac0);
equation(q_mac, (450.00, 1184.00), `\rho=\sec(\theta/3)`, 16);
color(q_mac, #a8e05a); hidden(q_mac);
text(n_mac, (450.00, 1202.00), "trisectrice de Mac-Laurin");
size(n_mac, 11); color(n_mac, fg); hidden(n_mac);
param(cay0, (603.23, 1112.00), 71.3743, 71.3743,
"(cos(t/3)*cos(t/3)*cos(t/3))*cos(t)", "(cos(t/3)*cos(t/3)*cos(t/3))*sin(t)", (-4.712389, 4.712389));
color(cay0, #a8e05a); stroke(cay0, 3); untraced(cay0);
equation(q_cay, (630.00, 1184.00), `\rho=\cos^3(\theta/3)`, 16);
color(q_cay, #a8e05a); hidden(q_cay);
text(n_cay, (630.00, 1202.00), "sextique de Cayley");
size(n_cay, 11); color(n_cay, fg); hidden(n_cay);
param(tsc0, (848.92, 1112.00), 13.0778, 13.0778,
"(1/(cos(t/3)*cos(t/3)*cos(t/3)))*cos(t)", "(1/(cos(t/3)*cos(t/3)*cos(t/3)))*sin(t)", (-3.060000, 3.060000));
color(tsc0, #a8e05a); stroke(tsc0, 3); untraced(tsc0);
equation(q_tsc, (810.00, 1184.00), `\rho=\sec^3(\theta/3)`, 16);
color(q_tsc, #a8e05a); hidden(q_tsc);
text(n_tsc, (810.00, 1202.00), "cubique de Tschirnhausen");
size(n_tsc, 11); color(n_tsc, fg); hidden(n_tsc);
param(str0, (975.63, 1112.23), 58.5285, 58.5285,
"(cos(2*t)/cos(t))*cos(t)", "(cos(2*t)/cos(t))*sin(t)", (0.000000, 1.050863));
color(str0, #6ba8f0); stroke(str0, 3); untraced(str0);
param(str1, (975.63, 1112.23), 58.5285, 58.5285,
"(cos(2*t)/cos(t))*cos(t)", "(cos(2*t)/cos(t))*sin(t)", (2.089159, 4.192455));
color(str1, #6ba8f0); stroke(str1, 3); untraced(str1);
param(str2, (975.63, 1112.23), 58.5285, 58.5285,
"(cos(2*t)/cos(t))*cos(t)", "(cos(2*t)/cos(t))*sin(t)", (5.232323, 6.283185));
color(str2, #6ba8f0); stroke(str2, 3); untraced(str2);
equation(q_str, (990.00, 1184.00), `\rho=\cos 2\theta/\cos\theta`, 16);
color(q_str, #6ba8f0); hidden(q_str);
text(n_str, (990.00, 1202.00), "strophoïde droite");
size(n_str, 11); color(n_str, fg); hidden(n_str);
param(hy10, (88.69, 1282.00), 2.6135, 2.6135,
"(cos(t)/cos(2*t))*cos(t)", "(cos(t)/cos(2*t))*sin(t)", (0.000000, 0.772832));
color(hy10, #6ba8f0); stroke(hy10, 3); untraced(hy10);
param(hy11, (88.69, 1282.00), 2.6135, 2.6135,
"(cos(t)/cos(2*t))*cos(t)", "(cos(t)/cos(2*t))*sin(t)", (0.797965, 2.343628));
color(hy11, #6ba8f0); stroke(hy11, 3); untraced(hy11);
param(hy12, (88.69, 1282.00), 2.6135, 2.6135,
"(cos(t)/cos(2*t))*cos(t)", "(cos(t)/cos(2*t))*sin(t)", (2.368761, 3.914424));
color(hy12, #6ba8f0); stroke(hy12, 3); untraced(hy12);
param(hy13, (88.69, 1282.00), 2.6135, 2.6135,
"(cos(t)/cos(2*t))*cos(t)", "(cos(t)/cos(2*t))*sin(t)", (3.939557, 5.485221));
color(hy13, #6ba8f0); stroke(hy13, 3); untraced(hy13);
param(hy14, (88.69, 1282.00), 2.6135, 2.6135,
"(cos(t)/cos(2*t))*cos(t)", "(cos(t)/cos(2*t))*sin(t)", (5.510354, 6.283185));
color(hy14, #6ba8f0); stroke(hy14, 3); untraced(hy14);
equation(q_hy1, (90.00, 1354.00), `\rho=\cos\theta/\cos 2\theta`, 16);
color(q_hy1, #6ba8f0); hidden(q_hy1);
text(n_hy1, (90.00, 1372.00), "hyperbole");
size(n_hy1, 11); color(n_hy1, fg); hidden(n_hy1);
param(bow0, (270.00, 1282.00), 50.4423, 50.4423,
"(cos(2*t)/(cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)))*sin(t)", (0.000000, 0.981748));
color(bow0, #6ba8f0); stroke(bow0, 3); untraced(bow0);
param(bow1, (270.00, 1282.00), 50.4423, 50.4423,
"(cos(2*t)/(cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)))*sin(t)", (2.161416, 4.123340));
color(bow1, #6ba8f0); stroke(bow1, 3); untraced(bow1);
param(bow2, (270.00, 1282.00), 50.4423, 50.4423,
"(cos(2*t)/(cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)))*sin(t)", (5.303008, 6.283185));
color(bow2, #6ba8f0); stroke(bow2, 3); untraced(bow2);
equation(q_bow, (270.00, 1354.00), `\rho=\cos 2\theta/\cos^2\theta`, 16);
color(q_bow, #6ba8f0); hidden(q_bow);
text(n_bow, (270.00, 1372.00), "nœud de papillon");
size(n_bow, 11); color(n_bow, fg); hidden(n_bow);
param(fpa0, (439.60, 1281.49), 61.5251, 61.5251,
"(cos(2*t)/(cos(t)*cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)*cos(t)))*sin(t)", (0.000000, 0.911062));
color(fpa0, #6ba8f0); stroke(fpa0, 3); untraced(fpa0);
param(fpa1, (439.60, 1281.49), 61.5251, 61.5251,
"(cos(2*t)/(cos(t)*cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)*cos(t)))*sin(t)", (2.232102, 4.052655));
color(fpa1, #6ba8f0); stroke(fpa1, 3); untraced(fpa1);
param(fpa2, (439.60, 1281.49), 61.5251, 61.5251,
"(cos(2*t)/(cos(t)*cos(t)*cos(t)))*cos(t)", "(cos(2*t)/(cos(t)*cos(t)*cos(t)))*sin(t)", (5.373694, 6.283185));
color(fpa2, #6ba8f0); stroke(fpa2, 3); untraced(fpa2);
equation(q_fpa, (450.00, 1354.00), `\rho=\cos 2\theta/\cos^3\theta`, 16);
color(q_fpa, #6ba8f0); hidden(q_fpa);
text(n_fpa, (450.00, 1372.00), "folium parabolique");
size(n_fpa, 11); color(n_fpa, fg); hidden(n_fpa);
param(bi20, (609.99, 1282.00), 80.0592, 80.0592,
"(sin(t)*sin(2*t))*cos(t)", "(sin(t)*sin(2*t))*sin(t)", (0.000000, 6.283185));
color(bi20, #6ba8f0); stroke(bi20, 3); untraced(bi20);
equation(q_bi2, (630.00, 1354.00), `\rho=\sin\theta\sin 2\theta`, 16);
color(q_bi2, #6ba8f0); hidden(q_bi2);
text(n_bi2, (630.00, 1372.00), "bifolium");
size(n_bi2, 11); color(n_bi2, fg); hidden(n_bi2);
param(cev0, (810.00, 1282.00), 17.3333, 17.3333,
"(sin(3*t)/sin(t))*cos(t)", "(sin(3*t)/sin(t))*sin(t)", (0.001571, 6.283185));
color(cev0, #f28aa8); stroke(cev0, 3); untraced(cev0);
equation(q_cev, (810.00, 1354.00), `\rho=\sin 3\theta/\sin\theta`, 16);
color(q_cev, #f28aa8); hidden(q_cev);
text(n_cev, (810.00, 1372.00), "trisectrice de Ceva");
size(n_cev, 11); color(n_cev, fg); hidden(n_cev);
param(ma20, (972.30, 1282.00), 31.9281, 31.9281,
"(sin(3*t)/sin(2*t))*cos(t)", "(sin(3*t)/sin(2*t))*sin(t)", (0.001571, 1.333606));
color(ma20, #f28aa8); stroke(ma20, 3); untraced(ma20);
param(ma21, (972.30, 1282.00), 31.9281, 31.9281,
"(sin(3*t)/sin(2*t))*cos(t)", "(sin(3*t)/sin(2*t))*sin(t)", (1.807987, 4.476770));
color(ma21, #f28aa8); stroke(ma21, 3); untraced(ma21);
param(ma22, (972.30, 1282.00), 31.9281, 31.9281,
"(sin(3*t)/sin(2*t))*cos(t)", "(sin(3*t)/sin(2*t))*sin(t)", (4.948008, 6.283185));
color(ma22, #f28aa8); stroke(ma22, 3); untraced(ma22);
equation(q_ma2, (990.00, 1354.00), `\rho=\sin 3\theta/\sin 2\theta`, 16);
color(q_ma2, #f28aa8); hidden(q_ma2);
text(n_ma2, (990.00, 1372.00), "trisectrice de Maclaurin");
size(n_ma2, 11); color(n_ma2, fg); hidden(n_ma2);
param(to10, (139.56, 1452.00), 23.1111, 23.1111,
"(sin(4*t)/sin(t))*cos(t)", "(sin(4*t)/sin(t))*sin(t)", (0.001571, 6.283185));
color(to10, #f28aa8); stroke(to10, 3); untraced(to10);
equation(q_to1, (180.00, 1524.00), `\rho=\sin 4\theta/\sin\theta`, 16);
color(q_to1, #f28aa8); hidden(q_to1);
text(n_to1, (180.00, 1542.00), "torpille");
size(n_to1, 11); color(n_to1, fg); hidden(n_to1);
param(to20, (319.56, 1452.00), 92.4444, 92.4444,
"(cos(2*t)*cos(t))*cos(t)", "(cos(2*t)*cos(t))*sin(t)", (0.000000, 6.283185));
color(to20, #f28aa8); stroke(to20, 3); untraced(to20);
equation(q_to2, (360.00, 1524.00), `\rho=\cos 2\theta\cos\theta`, 16);
color(q_to2, #f28aa8); hidden(q_to2);
text(n_to2, (360.00, 1542.00), "torpille");
size(n_to2, 11); color(n_to2, fg); hidden(n_to2);
param(tr20, (540.00, 1452.00), 1.0611, 1.0611,
"(1/(cos(2*t)*cos(t)))*cos(t)", "(1/(cos(2*t)*cos(t)))*sin(t)", (0.000000, 0.771261));
color(tr20, #f28aa8); stroke(tr20, 3); untraced(tr20);
param(tr21, (540.00, 1452.00), 1.0611, 1.0611,
"(1/(cos(2*t)*cos(t)))*cos(t)", "(1/(cos(2*t)*cos(t)))*sin(t)", (0.801106, 1.550376));
color(tr21, #f28aa8); stroke(tr21, 3); untraced(tr21);
param(tr22, (540.00, 1452.00), 1.0611, 1.0611,
"(1/(cos(2*t)*cos(t)))*cos(t)", "(1/(cos(2*t)*cos(t)))*sin(t)", (1.591217, 2.342057));
color(tr22, #f28aa8); stroke(tr22, 3); untraced(tr22);
param(tr23, (540.00, 1452.00), 1.0611, 1.0611,
"(1/(cos(2*t)*cos(t)))*cos(t)", "(1/(cos(2*t)*cos(t)))*sin(t)", (2.370332, 3.912854));
color(tr23, #f28aa8); stroke(tr23, 3); untraced(tr23);
param(tr24, (540.00, 1452.00), 1.0611, 1.0611,
"(1/(cos(2*t)*cos(t)))*cos(t)", "(1/(cos(2*t)*cos(t)))*sin(t)", (3.942699, 4.691969));
color(tr24, #f28aa8); stroke(tr24, 3); untraced(tr24);
param(tr25, (540.00, 1452.00), 1.0611, 1.0611,
"(1/(cos(2*t)*cos(t)))*cos(t)", "(1/(cos(2*t)*cos(t)))*sin(t)", (4.732809, 5.483650));
color(tr25, #f28aa8); stroke(tr25, 3); untraced(tr25);
param(tr26, (540.00, 1452.00), 1.0611, 1.0611,
"(1/(cos(2*t)*cos(t)))*cos(t)", "(1/(cos(2*t)*cos(t)))*sin(t)", (5.511924, 6.283185));
color(tr26, #f28aa8); stroke(tr26, 3); untraced(tr26);
equation(q_tr2, (540.00, 1524.00), `\rho=1/(\cos 2\theta\cos\theta)`, 16);
color(q_tr2, #f28aa8); hidden(q_tr2);
text(n_tr2, (540.00, 1542.00), "cf. trèfle équilatère");
size(n_tr2, 11); color(n_tr2, fg); hidden(n_tr2);
param(coc0, (686.56, 1482.96), 85.4598, 85.4598,
"(sin(t)/t)*cos(t)", "(sin(t)/t)*sin(t)", (0.020000, 9.424778));
color(coc0, #46c9b0); stroke(coc0, 3); untraced(coc0);
equation(q_coc, (720.00, 1524.00), `\rho=\sin\theta/\theta`, 16);
color(q_coc, #46c9b0); hidden(q_coc);
text(n_coc, (720.00, 1542.00), "cochléoïde");
size(n_coc, 11); color(n_coc, fg); hidden(n_coc);
param(din0, (934.31, 1452.00), 17.6897, 17.6897,
"(t/sin(t))*cos(t)", "(t/sin(t))*sin(t)", (-2.644854, -0.001556));
color(din0, #46c9b0); stroke(din0, 3); untraced(din0);
param(din1, (934.31, 1452.00), 17.6897, 17.6897,
"(t/sin(t))*cos(t)", "(t/sin(t))*sin(t)", (0.001556, 2.644854));
color(din1, #46c9b0); stroke(din1, 3); untraced(din1);
equation(q_din, (900.00, 1524.00), `\rho=\theta/\sin\theta`, 16);
color(q_din, #46c9b0); hidden(q_din);
text(n_din, (900.00, 1542.00), "quadratrice de Dinostrate");
size(n_din, 11); color(n_din, fg); hidden(n_din);
param(arc0, (175.32, 1617.29), 3.0070, 3.0070,
"(t)*cos(t)", "(t)*sin(t)", (0.020000, 18.849556));
color(arc0, #c8ccd8); stroke(arc0, 3); untraced(arc0);
equation(q_arc, (180.00, 1694.00), `\rho=\theta`, 16);
color(q_arc, #c8ccd8); hidden(q_arc);
text(n_arc, (180.00, 1712.00), "spirale d'Archimède");
size(n_arc, 11); color(n_arc, fg); hidden(n_arc);
param(gal0, (350.81, 1613.51), 0.1722, 0.1722,
"(t*t)*cos(t)", "(t*t)*sin(t)", (0.020000, 18.849556));
color(gal0, #c8ccd8); stroke(gal0, 3); untraced(gal0);
equation(q_gal, (360.00, 1694.00), `\rho=\theta^2`, 16);
color(q_gal, #c8ccd8); hidden(q_gal);
text(n_gal, (360.00, 1712.00), "spirale de Galilée");
size(n_gal, 11); color(n_gal, fg); hidden(n_gal);
param(hyp0, (523.47, 1653.20), 95.7586, 95.7586,
"(1/t)*cos(t)", "(1/t)*sin(t)", (0.905530, 18.849556));
color(hyp0, #c8ccd8); stroke(hyp0, 3); untraced(hyp0);
equation(q_hyp, (540.00, 1694.00), `\rho=\tfrac{1}{\theta}`, 16);
color(q_hyp, #c8ccd8); hidden(q_hyp);
text(n_hyp, (540.00, 1712.00), "spirale hyperbolique");
size(n_hyp, 11); color(n_hyp, fg); hidden(n_hyp);
param(fer0, (720.00, 1622.00), 13.1203, 13.1203,
"(sqrt(t))*cos(t)", "(sqrt(t))*sin(t)", (0.001000, 15.707963));
color(fer0, #c8ccd8); stroke(fer0, 3); untraced(fer0);
param(fer1, (720.00, 1622.00), 13.1203, 13.1203,
"((0 - sqrt(t)))*cos(t)", "((0 - sqrt(t)))*sin(t)", (0.001000, 15.707963));
color(fer1, #c8ccd8); stroke(fer1, 3); untraced(fer1);
equation(q_fer, (720.00, 1694.00), `\rho^2=\theta`, 16);
color(q_fer, #c8ccd8); hidden(q_fer);
text(n_fer, (720.00, 1712.00), "spirale de Fermat");
size(n_fer, 11); color(n_fer, fg); hidden(n_fer);
param(lit0, (900.00, 1622.00), 61.0873, 61.0873,
"(sqrt(1/t))*cos(t)", "(sqrt(1/t))*sin(t)", (0.772782, 12.566371));
color(lit0, #c8ccd8); stroke(lit0, 3); untraced(lit0);
param(lit1, (900.00, 1622.00), 61.0873, 61.0873,
"((0 - sqrt(1/t)))*cos(t)", "((0 - sqrt(1/t)))*sin(t)", (0.772782, 12.566371));
color(lit1, #c8ccd8); stroke(lit1, 3); untraced(lit1);
equation(q_lit, (900.00, 1694.00), `\rho^2=\tfrac{1}{\theta}`, 16);
color(q_lit, #c8ccd8); hidden(q_lit);
text(n_lit, (900.00, 1712.00), "lituus");
size(n_lit, 11); color(n_lit, fg); hidden(n_lit);
// the plate fills in one family at a time, in the order the table lists them
seq {
// 1 — powers of cos, and their inverses
par {
draw(cer0, 1.5);
show(q_cer, 0.5); show(n_cer, 0.5);
draw(dro0, 1.5);
show(q_dro, 0.5); show(n_dro, 0.5);
draw(con0, 1.5);
show(q_con, 0.5); show(n_con, 0.5);
draw(oeu0, 1.5);
show(q_oeu, 0.5); show(n_oeu, 0.5);
draw(kam0, 1.5);
show(q_kam, 0.5); show(n_kam, 0.5);
draw(dip0, 1.5);
draw(dip1, 1.5);
draw(dip2, 1.5);
draw(dip3, 1.5);
show(q_dip, 0.5); show(n_dip, 0.5);
draw(kul0, 1.5);
draw(kul1, 1.5);
draw(kul2, 1.5);
draw(kul3, 1.5);
show(q_kul, 0.5); show(n_kul, 0.5);
draw(fos0, 1.5);
show(q_fos, 0.5); show(n_fos, 0.5);
draw(dup0, 1.5);
show(q_dup, 0.5); show(n_dup, 0.5);
draw(agn0, 1.5);
draw(agn1, 1.5);
draw(agn2, 1.5);
show(q_agn, 0.5); show(n_agn, 0.5);
}
// 2 — sin and cos mixed
par {
draw(par0, 1.5);
draw(par1, 1.5);
show(q_par, 0.5); show(n_par, 0.5);
draw(cis0, 1.5);
draw(cis1, 1.5);
draw(cis2, 1.5);
show(q_cis, 0.5); show(n_cis, 0.5);
draw(bif0, 1.5);
show(q_bif, 0.5); show(n_bif, 0.5);
draw(mix0, 1.5);
draw(mix1, 1.5);
draw(mix2, 1.5);
draw(mix3, 1.5);
show(q_mix, 0.5); show(n_mix, 0.5);
draw(sem0, 1.5);
draw(sem1, 1.5);
draw(sem2, 1.5);
show(q_sem, 0.5); show(n_sem, 0.5);
}
// 3 — tangents
par {
draw(kap0, 1.5);
draw(kap1, 1.5);
draw(kap2, 1.5);
show(q_kap, 0.5); show(n_kap, 0.5);
draw(mou0, 1.5);
draw(mou1, 1.5);
draw(mou2, 1.5);
draw(mou3, 1.5);
draw(mou4, 1.5);
show(q_mou, 0.5); show(n_mou, 0.5);
draw(ser0, 1.5);
draw(ser1, 1.5);
draw(ser2, 1.5);
draw(ser3, 1.5);
show(q_ser, 0.5); show(n_ser, 0.5);
draw(swa0, 1.5);
draw(swa1, 1.5);
draw(swa2, 1.5);
draw(swa3, 1.5);
draw(swa4, 1.5);
draw(swa5, 1.5);
draw(swa6, 1.5);
draw(swa7, 1.5);
show(q_swa, 0.5); show(n_swa, 0.5);
}
// 4 — cos 2θ
par {
draw(qua0, 1.5);
show(q_qua, 0.5); show(n_qua, 0.5);
draw(cru0, 1.5);
draw(cru1, 1.5);
draw(cru2, 1.5);
draw(cru3, 1.5);
draw(cru4, 1.5);
show(q_cru, 0.5); show(n_cru, 0.5);
draw(lem0, 1.5);
draw(lem1, 1.5);
draw(lem2, 1.5);
draw(lem3, 1.5);
draw(lem4, 1.5);
draw(lem5, 1.5);
show(q_lem, 0.5); show(n_lem, 0.5);
draw(hy20, 1.5);
draw(hy21, 1.5);
draw(hy22, 1.5);
draw(hy23, 1.5);
draw(hy24, 1.5);
draw(hy25, 1.5);
show(q_hy2, 0.5); show(n_hy2, 0.5);
draw(mal0, 1.5);
draw(mal1, 1.5);
draw(mal2, 1.5);
draw(mal3, 1.5);
draw(mal4, 1.5);
draw(mal5, 1.5);
draw(mal6, 1.5);
draw(mal7, 1.5);
draw(mal8, 1.5);
draw(mal9, 1.5);
show(q_mal, 0.5); show(n_mal, 0.5);
}
// 5 — cos 3θ
par {
draw(tri0, 1.5);
show(q_tri, 0.5); show(n_tri, 0.5);
draw(tre0, 1.5);
draw(tre1, 1.5);
draw(tre2, 1.5);
draw(tre3, 1.5);
draw(tre4, 1.5);
draw(tre5, 1.5);
draw(tre6, 1.5);
show(q_tre, 0.5); show(n_tre, 0.5);
draw(kie0, 1.5);
draw(kie1, 1.5);
draw(kie2, 1.5);
draw(kie3, 1.5);
draw(kie4, 1.5);
draw(kie5, 1.5);
draw(kie6, 1.5);
draw(kie7, 1.5);
show(q_kie, 0.5); show(n_kie, 0.5);
}
// 6 — half angle
par {
draw(dur0, 1.5);
show(q_dur, 0.5); show(n_dur, 0.5);
draw(del0, 1.5);
show(q_del, 0.5); show(n_del, 0.5);
draw(car0, 1.5);
show(q_car, 0.5); show(n_car, 0.5);
draw(pa20, 1.5);
show(q_pa2, 0.5); show(n_pa2, 0.5);
}
// 7 — third angle
par {
draw(lim0, 1.5);
show(q_lim, 0.5); show(n_lim, 0.5);
draw(mac0, 1.5);
show(q_mac, 0.5); show(n_mac, 0.5);
draw(cay0, 1.5);
show(q_cay, 0.5); show(n_cay, 0.5);
draw(tsc0, 1.5);
show(q_tsc, 0.5); show(n_tsc, 0.5);
}
// 8 — cos 2θ over powers of cos θ
par {
draw(str0, 1.5);
draw(str1, 1.5);
draw(str2, 1.5);
show(q_str, 0.5); show(n_str, 0.5);
draw(hy10, 1.5);
draw(hy11, 1.5);
draw(hy12, 1.5);
draw(hy13, 1.5);
draw(hy14, 1.5);
show(q_hy1, 0.5); show(n_hy1, 0.5);
draw(bow0, 1.5);
draw(bow1, 1.5);
draw(bow2, 1.5);
show(q_bow, 0.5); show(n_bow, 0.5);
draw(fpa0, 1.5);
draw(fpa1, 1.5);
draw(fpa2, 1.5);
show(q_fpa, 0.5); show(n_fpa, 0.5);
draw(bi20, 1.5);
show(q_bi2, 0.5); show(n_bi2, 0.5);
}
// 9 — ratios of sines
par {
draw(cev0, 1.5);
show(q_cev, 0.5); show(n_cev, 0.5);
draw(ma20, 1.5);
draw(ma21, 1.5);
draw(ma22, 1.5);
show(q_ma2, 0.5); show(n_ma2, 0.5);
draw(to10, 1.5);
show(q_to1, 0.5); show(n_to1, 0.5);
draw(to20, 1.5);
show(q_to2, 0.5); show(n_to2, 0.5);
draw(tr20, 1.5);
draw(tr21, 1.5);
draw(tr22, 1.5);
draw(tr23, 1.5);
draw(tr24, 1.5);
draw(tr25, 1.5);
draw(tr26, 1.5);
show(q_tr2, 0.5); show(n_tr2, 0.5);
}
// 10 — θ against sin θ
par {
draw(coc0, 1.5);
show(q_coc, 0.5); show(n_coc, 0.5);
draw(din0, 1.5);
draw(din1, 1.5);
show(q_din, 0.5); show(n_din, 0.5);
}
// 11 — spirals
par {
draw(arc0, 1.5);
show(q_arc, 0.5); show(n_arc, 0.5);
draw(gal0, 1.5);
show(q_gal, 0.5); show(n_gal, 0.5);
draw(hyp0, 1.5);
show(q_hyp, 0.5); show(n_hyp, 0.5);
draw(fer0, 1.5);
draw(fer1, 1.5);
show(q_fer, 0.5); show(n_fer, 0.5);
draw(lit0, 1.5);
draw(lit1, 1.5);
show(q_lit, 0.5); show(n_lit, 0.5);
}
}
wait(2.4);
prolate-cycloid
A moving train always has a part moving BACKWARDS, and this is which part. A railway wheel does not run on its tread alone — the FLANGE hangs below it, so that lip has d > r and the curve it draws dips under the rail and ties a loop. Differentiate x = rθ − d·sin θ: dx/dθ = r − d·cos θ is NEGATIVE whenever cos θ > r/d, and those stretches are drawn in red — genuinely moving backwards along the rail, in the ground frame, on a train going forwards. Not a technicality either: a real 920 mm wheel with a 28 mm flange gives r/d = 0.9426, so it runs backwards for 10.84% of every revolution. Below, the same formula at d < r, d = r and d > r — dip, cusp, loop — because only a loop can go backwards.
// prolate-cycloid — a moving train always has a part of it moving BACKWARDS, and this is
// which part. A point on a rolling wheel traces
//
// x(θ) = rθ - d·sin θ, y(θ) = r - d·cos θ
//
// with d its distance from the axle. On the tread (d = r) that is the ordinary cycloid,
// cusping onto the rail once a revolution — the faint curve here. But a railway wheel does
// not run on its tread alone: the FLANGE hangs below it, so that lip has d > r, and the
// curve it draws dips under the rail and ties a loop.
//
// The loop is the claim. Differentiate: dx/dθ = r - d·cos θ, which is NEGATIVE whenever
// cos θ > r/d — that is, while the flange is near the bottom of its travel. Those stretches
// are drawn in red. The point is genuinely moving backwards along the rail, in the frame of
// the ground, on a train going forwards.
//
// It is not a technicality either. A real 920 mm wheel with a 28 mm flange gives r/d =
// 0.9426, so the backwards arc runs to |θ| < 0.3404 rad: 10.84% of every revolution.
// Roughly a tenth of the time, every flange on the train is heading for the station behind.
// The flange here is drawn at d = 1.5r so the loop is big enough to see; at true scale the
// loop is there but tiny.
//
// The three below are the same formula at d < r, d = r and d > r: curtate, the cycloid
// itself, and prolate. Only the prolate one has a loop, and only a loop can go backwards.
//
// manic examples/prolate-cycloid.manic
title("A moving train always has a part moving backwards");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);
line(rail, (60, 620), (1020, 620));
color(rail, dim);
// ---- the wheel, its flange, and the two marked points ------------------------------
// `roll` does the mechanism: no slipping, and everything tagged with the wheel comes
// along, so the pens are simply entities placed where a pen would be.
circle(wheel, (100, 550), 70);
outlined(wheel); outline(wheel, dim); stroke(wheel, 3);
circle(lip, (100, 550), 105); // the flange circle, d = 1.5r
outlined(lip); outline(lip, dim); stroke(lip, 2); opacity(lip, 0.35);
circle(tread, (100, 620), 8); color(tread, gold); // on the rim
circle(flange, (100, 655), 9); color(flange, magenta); // outside it — the flange lip
tag(wheel, rig); tag(lip, rig); tag(tread, rig); tag(flange, rig);
trail(tr, tread, fg, 2); // the ordinary cycloid, for comparison
opacity(tr, 0.35);
// ---- the flange's own curve, coloured where it is running BACKWARDS ----------------
// this one stays a formula, because the colour has to change along the curve: `trail`
// draws one colour, and the whole point is which stretches are red
cloud(back, 9000, #ffffff, 1.0) {
let go = min(t/10, 1)*12.566371;
let th = go*(i/9000);
let x = 100 + 70*th - 105*sin(th);
let y = 620 - (70 - 105*cos(th));
let dir = step(0, 105*cos(th) - 70); // dx/dθ = r - d·cos θ < 0
let hue = 205 - 205*dir; // blue forwards, red backwards
let sat = 0.8;
let r = 2;
}
// ---- d < r, d = r, d > r: only the last one loops ----------------------------------
// θ runs -π..π so the interesting part — dip, cusp, loop — sits in the middle of each
// slot rather than split between its ends, with the rail drawn at y = 0
line(b1, (60, 1330), (332, 1330)); color(b1, dim); opacity(b1, 0.5);
line(b2, (404, 1330), (676, 1330)); color(b2, dim); opacity(b2, 0.5);
line(b3, (748, 1330), (1020, 1330)); color(b3, dim); opacity(b3, 0.5);
param(curt, (196, 1330), 46, 46, "t - 0.55*sin(t)", "1 - 0.55*cos(t)", (0 - 3.14159, 3.14159));
param(cyc, (540, 1330), 46, 46, "t - sin(t)", "1 - cos(t)", (0 - 3.14159, 3.14159));
param(prol, (884, 1330), 46, 46, "t - 1.5*sin(t)", "1 - 1.5*cos(t)", (0 - 3.14159, 3.14159));
gradient(curt, mint, cyan, blue);
gradient(cyc, gold, coral, red);
gradient(prol, magenta, violet, indigo);
stroke(curt, 3); stroke(cyc, 3); stroke(prol, 3);
untraced(curt); untraced(cyc); untraced(prol);
caption(head, "A moving train always has a part moving backwards", (540, 130), 26);
equation(eq, (540, 1560),
`x = r\theta - d\sin\theta,\quad y = r - d\cos\theta,\qquad \frac{dx}{d\theta} < 0 \iff \cos\theta > \frac{r}{d}`, 25);
roll(rig, rail, 879.6, 10, linear); // two revolutions: 2·2πr
wait(0.4);
par { draw(curt, 2.0); draw(cyc, 2.0); draw(prol, 2.0); }
wait(7);
catenary-roll
Roll a PARABOLA along a line and watch its FOCUS: it traces a catenary, the curve a chain hangs in. Galileo thought the hanging chain WAS a parabola and was wrong, so there is something quietly funny about the parabola being the thing that draws the curve it was mistaken for. It is exact — carry the focus through the rolling motion and it lands on y = a·cosh(x/a), checked to 6e-10 (the arc-length quadrature, not the geometry). Staying closed-form needs one substitution: parametrise by the catenary’s own coordinate w, so tan φ = sinh w, s = a(sinh w·cosh w + w), focus = (aw, a·cosh w) — no integral left in it. The dashed curve is drawn independently; the focus lands on it. Notice how far the parabola travels for how little the focus moves — that flatness near the vertex is why a hanging cable looks parabolic and fooled a careful man for sixty years.
// catenary-roll — roll a PARABOLA along a line and watch its FOCUS. It traces a catenary:
// the curve a chain hangs in.
//
// Two curves that have no business being related. One is the graph of a quadratic, rolled;
// the other is what gravity does to a hanging chain, y = a·cosh(x/a). Galileo thought the
// hanging chain WAS a parabola and was wrong — Huygens, Leibniz and Bernoulli settled it
// sixty years later — so there is something quietly funny about the parabola being the
// thing that draws the curve it was mistaken for.
//
// It is exact. Roll y = x²/4a along the line: the contact point at arc length s(u) lands at
// (s, 0) and the parabola turns by the tangent angle φ there. Carry the focus through that
// motion and it lands on y = a·cosh(x/a) — checked over the whole roll, maximum error
// 6e-10, which is the arc-length quadrature and not the geometry.
//
// Driving it needs one substitution to stay closed-form. Parametrise by the catenary's own
// coordinate w instead of by the contact point:
//
// tan φ = sinh w, s = a(sinh w · cosh w + w), focus = (a·w, a·cosh w)
//
// so the whole mechanism — contact, rotation, focus — is sinh and cosh of one number, no
// integral left in it. The dashed curve is a·cosh(x/a) drawn independently; the focus lands
// on it rather than being placed there.
//
// Notice how far the parabola travels for how little the focus moves: contact runs ±3.6a
// while the focus covers ±0.95a. The catenary near its vertex is flat, and that flatness is
// why a hanging cable looks parabolic and fooled a careful man for sixty years.
//
// Bottom right: the same curve as a chain, hung between two pins. Nothing computes it — the
// beads sit on a·cosh(x/a).
//
// manic examples/catenary-roll.manic
title("Roll a parabola and its focus draws a hanging chain");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);
line(ground, (60, 1150), (1020, 1150));
color(ground, dim);
// ---- the catenary, drawn independently, for the focus to land on -------------------
param(cat, (540, 1150), 170, 170, "t", "cosh(t)", (0 - 0.95, 0.95));
color(cat, dim);
dashed(cat);
stroke(cat, 3);
opacity(cat, 0.55);
// ---- the parabola, rolled: every point carried through the same rigid motion -------
cloud(para, 2600, #ffffff, 1.0) {
let w = 0 - 0.95 + 1.9*min(t/11, 1); // the catenary's own coordinate
let t0 = sinh(w); // contact parameter: tan φ = sinh w
let ph = atan(t0);
let s = 170*(sinh(w)*cosh(w) + w); // arc length rolled off
let v = 0 - 1.55 + 3.1*(i/2600);
let px = 2*170*v; // the parabola y = x²/4a
let py = 170*v*v;
let qx = 2*170*t0; // …and its point of contact
let qy = 170*t0*t0;
let rx = (px - qx)*cos(ph) + (py - qy)*sin(ph); // rotate so the tangent lies flat
let ry = 0 - (px - qx)*sin(ph) + (py - qy)*cos(ph);
let x = 540 + s + rx;
let y = 1150 - ry;
let sat = 0;
let r = 1.3;
let alpha = 0.6;
}
// ---- the focus, and the curve it leaves behind --------------------------------------
cloud(trace, 6000, #ffffff, 1.0) {
let w0 = 0 - 0.95 + 1.9*min(t/11, 1);
let w = 0 - 0.95 + (w0 + 0.95)*(i/6000);
let x = 540 + 170*w;
let y = 1150 - 170*cosh(w);
let hue = mod(40 + (w + 0.95)*70, 360);
let sat = 0.75;
let r = 2;
}
cloud(foc, 90, #ffffff, 1.0) {
let w = 0 - 0.95 + 1.9*min(t/11, 1);
let a = i/90*6.283185;
let rr = 8*(i/90);
let x = 540 + 170*w + rr*cos(a*9);
let y = 1150 - 170*cosh(w) + rr*sin(a*9);
let hue = 45;
let sat = 0.5;
let r = 2;
}
// the focal radius: focus back to the contact point, so the rolling is legible
cloud(spoke, 110, #ffffff, 1.0) {
let w = 0 - 0.95 + 1.9*min(t/11, 1);
let u = i/110;
let s = 170*(sinh(w)*cosh(w) + w);
let fx = 170*w;
let fy = 170*cosh(w);
let x = 540 + fx + u*(s - fx);
let y = 1150 - fy - u*(0 - fy);
let sat = 0;
let r = 1.2;
let alpha = 0.4;
}
// ---- and the same curve as a chain, hung between two pins --------------------------
cloud(chain, 420, #ffffff, 1.0) {
let u = 0 - 1.35 + 2.7*(i/420);
let x = 800 + 52*u;
let y = 1560 - 52*cosh(u) + 52*2.06; // hang it from the pins
let hue = 200;
let sat = 0.35;
let r = 3;
let alpha = min(max(t - 11.4, 0), 1)*0.9;
}
caption(head, "Roll a parabola and its focus draws a hanging chain", (540, 130), 26);
equation(eq, (540, 1700), `\text{focus} = (aw,\; a\cosh w),\qquad s = a(\sinh w\cosh w + w)`, 25);
wait(18);
tautochrone
Four beads, four heights, one arrival. Released from rest anywhere on an inverted cycloid, a bead reaches the bottom in the SAME time: T = π√(r/g), with no θ₀ in it anywhere. The reason is that arc length from the bottom obeys simple harmonic motion — s = 4r·cos(θ/2) makes the tangential gravity proportional to s, exactly like a spring — so the quarter period has the amplitude nowhere in it. A bead from high up covers more distance and moves faster by precisely the factor that cancels. Numerically checked at five release angles: the descent times agree to 3e-4 s, the resolution of the integral at its singular endpoint. The beads are not eased into arriving together; each runs θ(t) = 2·arccos(cos(θ₀/2)·cos ωt) and they cross the bottom on the same frame. This is why Huygens hung a pendulum between cycloidal cheeks in 1656 — a circular pendulum only keeps time for small swings.
// tautochrone — four beads, four different heights, one arrival. Released from rest
// anywhere on an inverted cycloid, a bead reaches the bottom in the SAME time:
//
// T = π·√(r/g) — no θ₀ in it anywhere
//
// That is the tautochrone property, and it is the cycloid's second famous trick. The first
// is the brachistochrone (examples/brachistochrone.manic): of all the paths between two
// points, the cycloid is the quickest to slide down. Same curve, different miracle.
//
// The reason is that on a cycloid, arc length from the bottom obeys simple harmonic motion.
// Measure s along the curve from the lowest point and s = 4r·cos(θ/2); the tangential
// component of gravity works out proportional to s, exactly like a spring, so
//
// s(t) = s₀·cos(ωt), ω = ½√(g/r)
//
// and the quarter period — rest to the bottom — is (π/2)/ω = π√(r/g), with the amplitude
// nowhere in it. A bead from high up covers more distance and moves faster by precisely the
// factor that cancels. Numerically integrated to check before this was written: released at
// θ₀ = 0.2, 0.8, 1.6, 2.4 and 3.0 the descent times agree to 3e-4 s, which is the resolution
// of the integral at its singular endpoint, not a spread in the answer.
//
// So the beads here are not eased or tweened into arriving together. Each one runs
// θ(t) = 2·arccos(cos(θ₀/2)·cos(ωt)), which is that harmonic motion converted back to the
// curve's own parameter — different amplitudes, one period, and they cross the bottom on the
// same frame every time.
//
// This is why Huygens hung a pendulum between cycloidal cheeks in 1656: a circular pendulum
// only keeps time for small swings, but a cycloidal one keeps time for any swing at all.
//
// manic examples/tautochrone.manic
title("Four beads, four heights, one arrival");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand); size(brand, 21); color(brand, fg); opacity(brand, 0.82);
// ---- the bowl: an inverted cycloid ------------------------------------------------
cloud(bowl, 3000, #ffffff, 1.0) {
let th = i/3000*6.283185;
let x = 540 + 150*(th - 3.141593 - sin(th));
let y = 480 + 150*(1 - cos(th));
let sat = 0;
let r = 1.6;
let alpha = 0.55;
}
// ---- the release heights, so the four starts are visibly different -----------------
line(h1, (60, 498), (72, 498)); color(h1, dim); opacity(h1, 0.7);
line(h2, (60, 586), (116, 586)); color(h2, dim); opacity(h2, 0.7);
line(h3, (60, 698), (241, 698)); color(h3, dim); opacity(h3, 0.7);
line(h4, (60, 772), (441, 772)); color(h4, dim); opacity(h4, 0.7);
// ---- four beads: same period, four amplitudes -------------------------------------
cloud(beads, 480, #ffffff, 1.0) {
let per = 120;
let b = (i - mod(i, per))/per; // which bead, 0..3
let th0 = 0.5 + b*0.77; // released from rest here
let om = 1.047198; // ω, so the quarter period is 1.5 s
let th = 2*acos(cos(th0/2)*cos(om*t)); // SHM in arc length, back to θ
let a = mod(i, per)/per*6.283185;
let rr = 11*(mod(i, per)/per); // a filled bead
let x = 540 + 150*(th - 3.141593 - sin(th)) + rr*cos(a*11);
let y = 480 + 150*(1 - cos(th)) + rr*sin(a*11);
let hue = mod(30 + b*72, 360);
let sat = 0.7;
let r = 2.2;
}
// ---- the bottom they share --------------------------------------------------------
cloud(base, 200, #ffffff, 1.0) {
let u = i/200;
let x = 540;
let y = 700 + u*110;
let sat = 0;
let r = 1.2;
let alpha = 0.3;
}
caption(head, "Four beads, four heights, one arrival", (540, 130), 27);
equation(eq, (540, 1180),
`s(t) = s_0\cos\omega t,\quad \omega = \tfrac12\sqrt{g/r} \;\Longrightarrow\; T = \pi\sqrt{r/g}`, 27);
equation(eq2, (540, 1300), `\theta(t) = 2\arccos\!\left(\cos\tfrac{\theta_0}{2}\cos\omega t\right)`, 24);
wait(19);
involute
The only roulette in the family where the rolling curve is a straight LINE — and the only one you own several hundred of. It is the path of the end of a taut string unwound from a circle, and both defining facts are shown rather than asserted: the straight part is always TANGENT (its dot product with the radius stays under 1e-14) and its length is exactly the arc unwound, a·t, to 1e-14. The circle is the EVOLUTE of its own involute — “unwinding” said backwards. It is also why gears work: cut a tooth flank as an involute of a base circle and two wheels turn at an exactly constant velocity ratio, with the contact point running along the common tangent of the base circles — a straight line of action. Move the shafts further apart and the ratio does NOT change. No other profile tolerates that, which is why essentially every gear ever cut is an involute one. Left panel: the definition all at once, twenty taut strings whose endpoints all land on one curve. Right: fourteen involutes of one base circle — cut away past the tips and that is a gear.
// involute — the only roulette in this family where the rolling curve is a straight LINE
// rather than a circle, and the only one you own several hundred of. It is the path of the
// end of a taut string being unwound from a circle:
//
// x(t) = a(cos t + t·sin t)
// y(t) = a(sin t - t·cos t)
//
// Two things define it, and both are visible here rather than asserted. The string is
// always TANGENT to the circle — the straight part meets the radius at a right angle — and
// its length is exactly the arc it has unwound, a·t. Checked over the whole range: the
// segment's length matches a·t to 1e-14, and its dot product with the radius stays under
// 1e-14. That is the whole curve; everything else follows.
//
// It is a roulette in the same sense as the rest of the family. Rolling a line along the
// outside of the circle and marking a point of the line traces this; the circle is the
// EVOLUTE of its own involute, which is what "unwinding" means said backwards.
//
// And it is the reason gears work. Cut a tooth flank as an involute of a base circle and
// two such wheels turn with an exactly constant velocity ratio — the contact point runs
// along the common tangent of the two base circles, a straight line of action, and the
// ratio depends only on the base radii. Move the shafts slightly further apart and the
// ratio does NOT change; the contact point simply slides along the same line. No other
// profile tolerates that, which is why essentially every gear ever cut is an involute one.
// The panel on the right is where a gear comes from: fourteen involutes of one base circle,
// evenly spaced. Cut away everything past the tips and that is a gear.
//
// The panel on the left is the definition drawn all at once — a fan of taut strings at
// twenty unwind angles. Every one is tangent, every one is as long as the arc behind it,
// and every endpoint lands on the same curve.
//
// manic examples/involute.manic
title("An involute: a string unwinding, and every gear tooth ever cut");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// ---- the circle the string is wound on ---------------------------------------------
circle(spool, (540, 700), 60);
outlined(spool);
color(spool, dim);
// ---- the part still wrapped: an arc shrinking as the string comes off ---------------
cloud(wrapped, 900, #ffffff, 1.0) {
let go = min(t/9, 1)*6.283185; // how far it has unwound
let u = i/900;
let ph = go + u*(6.283185 - go); // what is left on the spool
let x = 540 + 60*cos(ph);
let y = 700 - 60*sin(ph);
let hue = 45;
let sat = 0.4;
let r = 1.6;
let alpha = 0.55;
}
// ---- the straight part: tangent at the contact point, as long as the arc unwound ----
cloud(string, 300, #ffffff, 1.0) {
let go = min(t/9, 1)*6.283185;
let u = i/300;
let tx = cos(go); // the tangent point
let ty = sin(go);
let ex = tx + go*sin(go); // …and the free end, a·t along the
let ey = ty - go*cos(go); // tangent direction
let x = 540 + 60*(tx + u*(ex - tx));
let y = 700 - 60*(ty + u*(ey - ty));
let hue = 45;
let sat = 0.55;
let r = 1.5;
let alpha = 0.85;
}
// ---- the involute the free end draws ------------------------------------------------
cloud(trace, 9000, #ffffff, 1.0) {
let go = min(t/9, 1)*6.283185;
let th = go*(i/9000);
let x = 540 + 60*(cos(th) + th*sin(th));
let y = 700 - 60*(sin(th) - th*cos(th));
let hue = mod(196 + th*16, 360);
let sat = 0.72;
let r = 1.8;
}
cloud(nib, 90, #ffffff, 1.0) {
let go = min(t/9, 1)*6.283185;
let a = i/90*6.283185;
let rr = 7*(i/90);
let x = 540 + 60*(cos(go) + go*sin(go)) + rr*cos(a*9);
let y = 700 - 60*(sin(go) - go*cos(go)) + rr*sin(a*9);
let hue = 45;
let sat = 0.5;
let r = 1.9;
}
// ---- left: the definition all at once — twenty taut strings, every end on the curve -
circle(spool2, (280, 1400), 26);
outlined(spool2);
color(spool2, dim);
opacity(spool2, 0.5);
cloud(fan, 2000, #ffffff, 1.0) {
let per = 100;
let c = (i - mod(i, per))/per; // which string, 0..19
let u = mod(i, per)/99;
let th = 6.283185*(c + 1)/20;
let tx = cos(th);
let ty = sin(th);
let ex = tx + th*sin(th);
let ey = ty - th*cos(th);
let x = 280 + 26*(tx + u*(ex - tx));
let y = 1400 - 26*(ty + u*(ey - ty));
let hue = mod(150 + c*9, 360);
let sat = 0.6;
let r = 1.1;
let alpha = min(max(min(max(t - 9.4, 0)/2.2, 1)*20 - c, 0), 1)*0.75;
}
// ---- right: where a gear comes from — fourteen involutes of one base circle ---------
circle(base, (800, 1400), 46);
outlined(base);
color(base, dim);
opacity(base, 0.5);
cloud(flanks, 2800, #ffffff, 1.0) {
let per = 200;
let f = (i - mod(i, per))/per; // which flank, 0..13
let u = mod(i, per)/199;
let th = u*2.0; // out to the tooth tip
let ix = cos(th) + th*sin(th); // the involute, then turned into place
let iy = sin(th) - th*cos(th);
let ph = f/14*6.283185;
let x = 800 + 46*(ix*cos(ph) - iy*sin(ph));
let y = 1400 - 46*(ix*sin(ph) + iy*cos(ph));
let hue = mod(30 + f*6, 360);
let sat = 0.65;
let r = 1.2;
let alpha = min(max(min(max(t - 9.4, 0)/2.2, 1)*14 - f, 0), 1)*0.8;
}
// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "An involute: a string unwinding, and every gear tooth ever cut", (540, 130), 24);
equation(eq, (540, 1650), `x = a(\cos t + t\sin t),\qquad y = a(\sin t - t\cos t)`, 27);
// the string unwinds from t = 0; the two panels fill in once it is fully off
wait(13);
cardioid
A cardioid drawn with nothing but STRAIGHT LINES. Mark N points round a circle and join every point n to point 2n — no curve is drawn anywhere, and the cardioid is the envelope the chords leave behind. The multiplier is the whole story: joining n to k·n envelopes an epicycloid with k−1 cusps, computed here by taking the envelope as the limit of intersections of neighbouring chords (1, 2, 3, 4 cusps for k = 2, 3, 4, 5). Fitting the ×2 envelope gives b = |a| — the cardioid condition — about a centre one third back along the axis: r = ⅔(1 + cos φ), cusping at x = −⅓ and touching the circle at (1,0). That exact curve is drawn over the chords at the end, and it lies on the envelope they already made. The row below carries on with ×3, ×4, ×5.
// cardioid — drawn with nothing but straight lines. Mark N points evenly round a circle,
// number them, and join every point n to point 2n. No curve is drawn anywhere; the
// cardioid is the ENVELOPE the chords leave behind.
//
// It is the two times table, and the multiplier is the whole story: joining n to k·n
// envelopes an epicycloid with k-1 cusps. Computed here rather than asserted — the
// envelope was found as the limit of intersections of neighbouring chords, and counting
// its local maxima gives 1, 2, 3, 4 cusps for k = 2, 3, 4, 5. So ×2 is this cardioid, ×3
// is a nephroid (see examples/nephroid.manic), and the row along the bottom carries on.
//
// Which cardioid, exactly? Fitting the ×2 envelope to r = b + a·cos φ lands on b = |a| —
// the cardioid condition — about a centre one third of the way back along the axis:
//
// r = (2/3)·(1 + cos φ), centred at (-1/3, 0)
//
// so it cusps at x = -1/3 and touches the circle at (1, 0). That exact curve is drawn over
// the chords at the end, and it lies on the envelope they already made.
//
// The other definition is the rolling one: a cardioid is the epicycloid traced by a point
// on a circle rolling around another of EQUAL radius — the limaçon of
// examples/limacon.manic at the hinge, where the pen sits exactly on the rim and the
// dimple has closed into a cusp but not yet opened into a loop.
//
// Nothing here is a curve primitive. Every chord is a straight run of points, appearing
// one after another as `alpha` crosses each chord's index, and the shape is what is left
// between them.
//
// manic examples/cardioid.manic
title("A cardioid, from the two times table");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// ---- the circle the points sit on --------------------------------------------------
circle(ring, (540, 700), 330);
outlined(ring);
color(ring, dim);
opacity(ring, 0.5);
// ---- 180 chords, n -> 2n, arriving one at a time ------------------------------------
cloud(chords, 5400, #ffffff, 1.0) {
let per = 30;
let c = (i - mod(i, per))/per; // which chord, 0..179
let u = mod(i, per)/29; // …how far along it
let th = 6.283185*c/180;
let ax = cos(th);
let ay = sin(th);
let bx = cos(2*th); // the two times table: n -> 2n
let by = sin(2*th);
let x = 540 + 330*(ax + u*(bx - ax));
let y = 700 - 330*(ay + u*(by - ay));
let hue = mod(200 + c*1.6, 360); // hue by chord, so the order reads
let sat = 0.7;
let r = 1.2;
let alpha = min(max(min(t/9, 1)*180 - c, 0), 1)*0.55;
}
// ---- the curve those chords were hiding: r = (2/3)(1 + cos φ) about (-1/3, 0) -------
polar(card, (430, 700), 330, 330, "0.6667*(1 + cos(t))");
color(card, gold);
stroke(card, 4);
untraced(card);
// ---- x3, x4, x5: the same rule, one cusp more each time ----------------------------
cloud(k3, 2400, #ffffff, 1.0) {
let per = 20;
let c = (i - mod(i, per))/per;
let u = mod(i, per)/19;
let th = 6.283185*c/120;
let ax = cos(th);
let ay = sin(th);
let bx = cos(3*th);
let by = sin(3*th);
let x = 196 + 118*(ax + u*(bx - ax));
let y = 1420 - 118*(ay + u*(by - ay));
let hue = 175;
let sat = 0.65;
let r = 1.1;
let alpha = min(max(min(max(t - 9.2, 0)/2.2, 1)*120 - c, 0), 1)*0.6;
}
cloud(k4, 2400, #ffffff, 1.0) {
let per = 20;
let c = (i - mod(i, per))/per;
let u = mod(i, per)/19;
let th = 6.283185*c/120;
let ax = cos(th);
let ay = sin(th);
let bx = cos(4*th);
let by = sin(4*th);
let x = 540 + 118*(ax + u*(bx - ax));
let y = 1420 - 118*(ay + u*(by - ay));
let hue = 32;
let sat = 0.65;
let r = 1.1;
let alpha = min(max(min(max(t - 9.2, 0)/2.2, 1)*120 - c, 0), 1)*0.6;
}
cloud(k5, 2400, #ffffff, 1.0) {
let per = 20;
let c = (i - mod(i, per))/per;
let u = mod(i, per)/19;
let th = 6.283185*c/120;
let ax = cos(th);
let ay = sin(th);
let bx = cos(5*th);
let by = sin(5*th);
let x = 884 + 118*(ax + u*(bx - ax));
let y = 1420 - 118*(ay + u*(by - ay));
let hue = 300;
let sat = 0.65;
let r = 1.1;
let alpha = min(max(min(max(t - 9.2, 0)/2.2, 1)*120 - c, 0), 1)*0.6;
}
// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "A cardioid, from the two times table", (540, 130), 27);
equation(eq, (540, 1640), `n \mapsto 2n \;\Longrightarrow\; r = \tfrac{2}{3}(1+\cos\varphi)`, 30);
// the chords arrive from t = 0; the curve they were hiding is drawn over them at the end
wait(9.6);
draw(card, 2.2);
wait(8);
nephroid
The bright curve in the bottom of a coffee cup. What is drawn is not the formula — it is LIGHT: parallel rays cross the cup, strike the far wall and reflect once, each reflection worked out properly as d′ = d − 2(d·n)n, and the curve appears where the reflected rays crowd together. Nobody draws it; it is an envelope, a caustic. That it IS a nephroid is checked rather than quoted: every envelope point satisfies (x²+y²−4a²)³ = 108a⁴y² with a = ¼ to a residual of 5e-12, and lies within 3e-4 of x = 3a·cos t − a·cos 3t. So the cusps sit at ±R/2, and one direction of light makes exactly HALF the nephroid — the faint dashed curve is the whole one, so you can see which half. Below: the same curve from the THREE times table (chords n → 3n, envelope = epicycloid with 3−1 = 2 cusps), and from one line of complex arithmetic — z → z³ + 3z sends the UNIT CIRCLE to a nephroid (residual 2e-12), so that panel starts as the circle and lets the map carry every point to where it belongs. The parametrisation reduces too: y is the single cube 4a·sin³φ.
// nephroid — the bright curve in the bottom of a coffee cup, and the same curve three
// other ways. It is the epicycloid with two cusps: a circle rolling around another of
// TWICE its radius, pen on the rim.
//
// x(φ) = 3a·cos φ - a·cos 3φ = 6a·cos φ - 4a·cos³φ
// y(φ) = 3a·sin φ - a·sin 3φ = 4a·sin³φ
//
// Those reductions are exact — checked to 2e-15 across the whole parameter range — and the
// second is the tidier of the pair: the whole y coordinate is one cube.
//
// What is drawn here is not that formula. It is LIGHT. Parallel rays cross the cup, strike
// the far wall and reflect once — each reflection worked out properly, d' = d - 2(d·n)n
// with n the outward normal at the point of contact — and the curve appears where the
// reflected rays crowd together. Nobody draws it; it is an envelope, the caustic.
//
// That the caustic IS a nephroid is checked, not quoted. Taking the envelope as the limit
// of intersections of neighbouring reflected rays, for a cup of radius 1:
//
// · every envelope point satisfies (x² + y² - 4a²)³ = 108a⁴y² with a = 1/4, to a
// residual of 5e-12
// · and lies within 3e-4 of the parametrisation above — which is the sampling step,
// not an error
//
// So the cusps sit at ±R/2 and the horns reach the rim, and one direction of light makes
// exactly HALF the nephroid. The faint dashed curve is the whole one, so you can see which
// half the light is responsible for.
//
// The row below is the same nephroid twice more, because curves this old have many
// constructions. On the left, chords joining n to 3n around a circle — the THREE times
// table, whose envelope is the epicycloid with 3-1 = 2 cusps (the two times table gives
// the cardioid in examples/cardioid.manic).
//
// On the right, one line of complex arithmetic: the map z → z³ + 3z sends the UNIT CIRCLE
// to a nephroid. Put z = e^(iφ) and the image is (3cos φ + cos 3φ, 3sin φ + sin 3φ), which
// is the parametrisation above turned a half turn — cusps at ±2a, horns at ±4a, and it
// satisfies the nephroid's implicit equation to a residual of 2e-12. So the panel does not
// draw the answer: it starts as the unit circle and lets the map carry every point to
// where it belongs.
//
// manic examples/nephroid.manic
title("A nephroid: the caustic in a coffee cup");
canvas("9:16");
template("black");
bloom(0.32, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// ---- the cup -----------------------------------------------------------------------
circle(cup, (540, 700), 330);
outlined(cup);
color(cup, dim);
// ---- the light arriving: 90 parallel rays, crossing to the far wall -----------------
cloud(incoming, 2700, #ffffff, 1.0) {
let per = 30;
let c = (i - mod(i, per))/per; // which ray, 0..89
let u = mod(i, per)/29;
let h = 0 - 0.985 + 1.97*c/89; // its height in the cup
let w = sqrt(1 - h*h);
let x0 = 0 - 1.34; // start outside the rim
let x1 = w; // …to the far wall
let x = 540 + 330*(x0 + u*(x1 - x0));
let y = 700 - 330*h;
let hue = 48;
let sat = 0.35;
let r = 1.1;
let alpha = min(max(min(t/3, 1)*90 - c, 0), 1)*0.30;
}
// ---- and reflecting once: d' = d - 2(d.n)n, worked out per ray ---------------------
cloud(reflected, 5400, #ffffff, 1.0) {
let per = 60;
let c = (i - mod(i, per))/per;
let u = mod(i, per)/59;
let h = 0 - 0.985 + 1.97*c/89;
let w = sqrt(1 - h*h);
let dx = 2*h*h - 1; // the reflected direction, unit length
let dy = 0 - 2*h*w;
let len = 2*w; // chord back across to the rim
let x = 540 + 330*(w + u*len*dx);
let y = 700 - 330*(h + u*len*dy);
let hue = mod(40 + c*0.9, 360);
let sat = 0.72;
let r = 1.1;
let alpha = min(max(min(max(t - 1.2, 0)/4, 1)*90 - c, 0), 1)*0.5;
}
// ---- the whole nephroid, so you can see which half the light made ------------------
param(neph, (540, 700), 330, 330,
"3*0.25*cos(t) - 0.25*cos(3*t)", "3*0.25*sin(t) - 0.25*sin(3*t)", (0, 6.283185));
color(neph, dim);
dashed(neph);
opacity(neph, 0.55);
stroke(neph, 3);
// ---- the three times table: chords n -> 3n, envelope = the same curve --------------
cloud(k3, 3000, #ffffff, 1.0) {
let per = 25;
let c = (i - mod(i, per))/per;
let u = mod(i, per)/24;
let th = 6.283185*c/120;
let ax = cos(th);
let ay = sin(th);
let bx = cos(3*th);
let by = sin(3*th);
let x = 300 + 150*(ax + u*(bx - ax));
let y = 1420 - 150*(ay + u*(by - ay));
let hue = 190;
let sat = 0.65;
let r = 1.1;
let alpha = min(max(min(max(t - 6, 0)/2.4, 1)*120 - c, 0), 1)*0.55;
}
// ---- and one line of complex arithmetic: z -> z^3 + 3z, applied to the unit circle --
circle(unitc, (790, 1420), 38); // where every point starts
outlined(unitc);
color(unitc, dim);
opacity(unitc, 0.4);
cloud(zmap, 3000, #ffffff, 1.0) {
let ph = i/3000*6.283185;
let u = min(max(t - 9, 0)/2.6, 1); // the map, applied over time
let zx = cos(ph); // z on the unit circle
let zy = sin(ph);
let wx = 3*cos(ph) + cos(3*ph); // z^3 + 3z, real part
let wy = 3*sin(ph) + sin(3*ph); // …and imaginary
let x = 790 + 38*(zx + u*(wx - zx));
let y = 1420 - 38*(zy + u*(wy - zy));
let hue = mod(24 + ph*22, 360);
let sat = 0.7;
let r = 1.3;
}
// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "A nephroid: the caustic in a coffee cup", (540, 130), 27);
equation(eq, (540, 1650), `x = 3a\cos\varphi - a\cos 3\varphi,\quad y = 4a\sin^3\varphi,\qquad z \mapsto z^3+3z`, 25);
// light arrives and reflects, the three times table fills in, and the unit circle is
// carried onto the same curve by z -> z^3 + 3z
wait(14);
limacon
The snail curve, r = b + a·cos θ — and also a roulette: a circle rolling around the OUTSIDE of a circle of EQUAL radius with a pen fixed to it, which is the epitrochoid with R = r. The two are the same curve exactly: rolling radius A and pen distance d give b = 2A, a = −2d, checked by pushing the roulette through the limaçon’s Cartesian form at five values of d with a residual under 4e-13. So b/|a| = A/d, and ONE number — where the pen sits — walks the curve through every shape it has: d < A/2 convex, A/2 < d < A dimpled, d = A the CARDIOID, d > A an inner loop. The cardioid is not a separate curve but the hinge, the single pen distance where a dimple has closed into a cusp on its way to becoming a loop. The row below is the polar form at ratios 2.8, 1.6, 1.0 and 0.6, each scaled to the same width because only the ratio matters. Pure visual: the mark, the title and the formula.
// limacon — the snail curve. In polar it is about as simple as a curve gets:
//
// r = b + a·cos θ
//
// and it is also a roulette: a circle rolling around the OUTSIDE of a circle of EQUAL
// radius, with a pen fixed to the rolling one. That is the epitrochoid of
// examples/epitrochoid.manic with R = r, and the two descriptions are the same curve
// exactly, not approximately. Rolling radius A, pen at distance d from the rolling
// centre, origin shifted by d along the axis:
//
// b = 2A, a = -2d
//
// Checked rather than quoted: the roulette points were pushed through the limaçon's
// Cartesian form (x² + y² - a·x)² = b²(x² + y²) at d = 0.35, 0.6, 1.0, 1.6 and 2.4, and
// the residual never exceeded 4e-13.
//
// So b/|a| = A/d, and ONE NUMBER — where the pen sits — walks the curve through every
// shape it has. Wikipedia's classification, read in terms of the pen:
//
// d < A/2 b > 2|a| convex, no dimple at all
// d = A/2 b = 2|a| a point of zero curvature appears
// A/2 < d < A |a| < b < 2|a| dimpled, an indentation between two inflection points
// d = A b = |a| the CARDIOID — the pen is exactly on the rim, and the
// dimple has closed into a cusp
// d > A b < |a| an inner loop, and the curve crosses itself
//
// The cardioid is the hinge: it is not a separate curve but the single value of d where a
// dimple becomes a cusp on its way to becoming a loop. Turning here is d = 1.6A, past that
// hinge, so the pen swings inside the track and cuts the loop.
//
// The four along the bottom are the polar form directly, each scaled so that b + |a| is
// the same width, because the SHAPE depends only on the ratio — 2.8, 1.6, 1.0 and 0.6
// against a = 1. Left to right: convex, dimpled, cardioid, looped.
//
// A limaçon is also the pedal curve of a circle, and the conchoid of a circle taken about
// a point on it. Three constructions, one curve.
//
// manic examples/limacon.manic
title("A limacon, and the one number that decides its shape");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// ---- a circle rolling on the OUTSIDE of one the same size --------------------------
// R = r is the whole trick, and `roll` gets it from where the wheel is put: outside the
// track. The pen sits 1.6A from the wheel's centre — past the rim, so the curve loops.
circle(track, (540, 700), 125);
outlined(track); outline(track, dim); stroke(track, 3);
circle(locus, (540, 700), 250); // R + r, the wheel centre's own circle
outlined(locus); outline(locus, dim); stroke(locus, 2); dashed(locus); opacity(locus, 0.4);
circle(wheel, (790, 700), 125);
outlined(wheel); outline(wheel, fg); stroke(wheel, 3); opacity(wheel, 0.8);
circle(pen, (590, 700), 9); color(pen, gold); // d = 1.6A, outside the rim
line(arm, wheel, pen); color(arm, dim); opacity(arm, 0.55);
tag(wheel, rig); tag(pen, rig); tag(arm, rig);
trail(trace, pen, magenta, 4);
// ---- r = b + a·cos θ, four times: convex, dimpled, cardioid, looped ----------------
// each scaled so b + |a| is the same width, because only the RATIO changes the shape
polar(conv, (168, 1420), 27, 27, "2.8 + cos(t)");
polar(dimp, (416, 1420), 39, 39, "1.6 + cos(t)");
polar(card, (664, 1420), 51, 51, "1.0 + cos(t)");
polar(loop, (912, 1420), 63, 63, "0.6 + cos(t)");
gradient(conv, mint, cyan, blue);
gradient(dimp, cyan, blue, violet);
gradient(card, gold, coral, red);
gradient(loop, magenta, violet, indigo);
stroke(conv, 3);
stroke(dimp, 3);
stroke(card, 3);
stroke(loop, 3);
untraced(conv);
untraced(dimp);
untraced(card);
untraced(loop);
// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "A limacon, and the one number that decides its shape", (540, 130), 26);
equation(eq, (540, 1620), `r = b + a\cos\theta,\qquad b = 2A,\quad a = -2d`, 30);
// the roll runs from t = 0; the four shapes draw once the loop has closed
roll(rig, track, 1, 8, linear); // (R+r)/r = 2, so one lap shuts it
wait(0.4);
par { // all four together, not in turn
draw(conv, 2.0);
draw(dimp, 2.0);
draw(card, 2.0);
draw(loop, 2.0);
}
wait(8);
spirograph
The toy, drawn honestly — a toothed ring, a toothed wheel inside it, a pen in one of the holes, on Wikipedia’s parameterisation (k = r/R, l = ρ/r). It is the hypotrochoid with the radii normalised, so why draw teeth? Because THE TEETH ARE THE MATHEMATICS. Arc length per tooth is 2πR/N_ring and 2πr/N_wheel, equal only when R/r = N_ring/N_wheel exactly — 96 and 36 both give 0.065449847, which is why the teeth on screen genuinely mesh at the contact point. And therefore the ratio is RATIONAL and the curve always closes: reduce by the gcd and the pen draws N_ring/g lobes in N_wheel/g laps. 96 and 36 share 12 — eight lobes, three laps. No slipping, no never-closing spiral; that is why the toy has teeth rather than rubber rims. The row below is the same ring with 24, 40 and 63 teeth, predicted entirely by their gcds: 4 lobes in 1 lap, 12 in 5, 32 in 21 — which is why the odd wheels make the densest rosettes. Pure visual: the mark, the title and the formula.
// 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 = r/R, 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 R/r = 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);
hypotrochoid
The hypocycloid with the pen taken OFF the rim — the Spirograph curve, where the only new number is d, how far the pen sits from the wheel’s centre. d = r puts it back on the rim (cusps); inside, the cusps round into a rosette; outside, they open into loops. What d CANNOT change is when the curve closes — that is the ratio alone, p lobes after q laps for R/r = p/q, whatever d is. The row below is the surprise, and it is exact: set R = 2r and the formula collapses to (r+d)·cos θ and (r−d)·sin θ for EVERY d — an ellipse with semi-axes r+d and |r−d| (verified to 7e-16). So the Tusi couple’s straight line is not a special construction at all: it is the member with d = r, where the minor axis goes to zero and the ellipse flattens onto its own major axis. Pure visual — the mark, the title and the formula — and the wheel never stops turning.
// hypotrochoid — the hypocycloid with the pen taken OFF the rim. Same wheel rolling
// inside the same track; the only new number is d, how far the pen sits from the wheel's
// centre. This is the Spirograph curve — the toy is a ring, a wheel and a choice of hole.
//
// x(θ) = (R-r)·cos θ + d·cos((R-r)/r · θ)
// y(θ) = (R-r)·sin θ - d·sin((R-r)/r · θ)
//
// Put d = r and the pen is back on the rim: cusps, a hypocycloid. Pull it inside (d < r)
// and the cusps round off into a smooth rosette — which is what is turning here, R/r = 7/3
// with d = 2. Push it outside (d > r) and they open into loops.
//
// What d CANNOT do is change when the curve closes. That is fixed by the ratio alone:
// write R/r in lowest terms as p/q and the pen comes home after q laps having drawn p
// lobes, whatever d is. Seven lobes in three laps here, so the first two laps visibly fail
// to join up.
//
// The row along the bottom is the surprise, and it is exact. Set R = 2r and the formula
// collapses for EVERY d:
//
// x = r·cos θ + d·cos θ = (r + d)·cos θ
// y = r·sin θ - d·sin θ = (r - d)·sin θ
//
// — an ELLIPSE with semi-axes r+d and |r-d|, whatever the pen's offset. Checked here to
// machine precision: |x²/a² + y²/b² - 1| stays under 7e-16 across the whole curve for
// d = 0.2, 0.5, 0.8 and 1.5. So every point rigidly attached to a wheel rolling inside a
// track of twice its radius travels an ellipse — and the straight line of the Tusi couple
// (see examples/hypocycloid.manic) is not a special construction at all, it is just the
// member of that family with d = r, where the minor axis r - d goes to zero and the
// ellipse flattens onto its own major axis.
//
// The wheel never stops. The curve shuts after three laps and the trace holds there, but
// the mechanism keeps turning for the rest of the scene, running the pen back around its
// own line.
//
// manic examples/hypotrochoid.manic
title("A hypotrochoid, and the ellipses hiding inside it");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// ---- the track, the wheel inside it, and a pen OFF the rim ------------------------
// the only difference from examples/hypocycloid.manic is where `pen` sits: inside the
// wheel instead of on it. `roll` neither knows nor cares — it carries whatever is tagged.
circle(track, (540, 760), 230);
outlined(track); outline(track, dim); stroke(track, 3);
circle(locus, (540, 760), 131);
outlined(locus); outline(locus, dim); stroke(locus, 2); dashed(locus); opacity(locus, 0.45);
circle(wheel, (671, 760), 98.6);
outlined(wheel); outline(wheel, fg); stroke(wheel, 3); opacity(wheel, 0.8);
circle(pen, (737, 760), 9); color(pen, gold); // d = 2 units, INSIDE the rim
line(arm, wheel, pen); color(arm, dim); opacity(arm, 0.55);
tag(wheel, rig); tag(pen, rig); tag(arm, rig);
trail(trace, pen, cyan, 4);
// ---- R = 2r: an ellipse for every d, and a line when d = r -------------------------
// left unsimplified on purpose — (r+d)cos θ and (r-d)sin θ are hiding in plain sight.
// d climbs left to right and the minor axis r-d shrinks with it, until at d = r it is
// zero and the ellipse has flattened onto its own major axis.
param(wide, (196, 1380), 60, 60, "cos(t) + 0.25*cos(t)", "sin(t) - 0.25*sin(t)", (0, 6.283185));
param(flat, (540, 1380), 60, 60, "cos(t) + 0.65*cos(t)", "sin(t) - 0.65*sin(t)", (0, 6.283185));
param(tall, (884, 1380), 60, 60, "cos(t) + 1.00*cos(t)", "sin(t) - 1.00*sin(t)", (0, 6.283185));
gradient(wide, mint, cyan, blue); // d = 0.25 -> 1.25 x 0.75
gradient(flat, gold, coral, red); // d = 0.65 -> 1.65 x 0.35
gradient(tall, magenta, violet, magenta); // d = 1.00 -> 2.00 x 0, the line
stroke(wide, 3);
stroke(flat, 3);
stroke(tall, 4);
untraced(wide);
untraced(flat);
untraced(tall);
// ---- nothing but the mark, the title and the formula -------------------------------
caption(head, "A hypotrochoid, and the ellipses hiding inside it", (540, 130), 27);
equation(eq, (540, 1600),
`x=(R{-}r)\cos\theta+d\cos\tfrac{R-r}{r}\theta,\quad y=(R{-}r)\sin\theta-d\sin\tfrac{R-r}{r}\theta`, 26);
// the roll runs from t = 0; the three ellipses draw once the rosette has closed
roll(rig, track, 3, 12, linear); // R/r = 7/3 needs three laps
wait(0.4);
draw(wide, 1.6);
draw(flat, 1.6);
draw(tall, 1.6);
wait(8);
hypocycloid
The epitrochoid’s mirror image: the wheel rolls around the INSIDE of the fixed circle, so the two rotations SUBTRACT and the curve bites inward into cusps instead of bulging into lobes. Write R/r in lowest terms as p/q and the pen comes home after q laps having cut p cusps — here 5/2, a five-pointed star that visibly fails to close on the first lap. The row along the bottom changes nothing but the ratio: R = 4r is the ASTROID (x reduces exactly to R·cos³θ), R = 3r the DELTOID — and R = 2r is a straight line. That is not a degenerate drawing: the sines cancel for every angle, y is 0 throughout, and every rim point runs the full diameter. Two circular motions with no curve left in them — the Tusi couple, thirteenth century, straight-line motion built out of circles. Pure visual: the mark, the title and the formula, and the rolling does the explaining.
// hypocycloid — the curve a pen on the rim draws when the wheel rolls around the INSIDE
// of a fixed circle. The epitrochoid's mirror image: same construction, wheel on the
// other side of the track.
//
// x(θ) = (R-r)·cos θ + r·cos((R-r)/r · θ)
// y(θ) = (R-r)·sin θ - r·sin((R-r)/r · θ)
//
// Two numbers decide everything. Write R/r in lowest terms as p/q: the pen comes home
// after q laps having cut p CUSPS into the fixed circle — points where it stops dead and
// reverses, which happen exactly when the pen is the contact point and its velocity is
// momentarily zero. Here R/r = 5/2, so five cusps take two laps, and the first lap
// visibly fails to close.
//
// The row along the bottom is what changes when only the RATIO changes:
//
// R = 4r the ASTROID — and x reduces exactly to R·cos³θ, y to R·sin³θ
// R = 3r the DELTOID, three cusps
// R = 2r a STRAIGHT LINE
//
// That last one is not a degenerate drawing, it is the whole point. Put the wheel at half
// the radius and y(θ) = r·sin θ - r·sin θ = 0 for every θ, while x sweeps the full
// diameter: every point on the rim runs back and forth along a straight line. Two circular
// motions, added, with no curve left in them. It is the Tusi couple, worked out in the
// thirteenth century to build straight-line motion out of circles, and it is the reason
// this family is worth showing rolling rather than plotted — nothing about the formula
// looks like a straight line until the wheel turns.
//
// The mechanism is not a drawing of a mechanism: the wheel, the spoke and the pen are all
// evaluated from the same θ as the curve, so the pen IS the leading end of the trace and
// nothing can drift. Rolling without slipping is again where the second angle comes from —
// the wheel's centre runs a circle of radius R-r while the wheel turns (R-r)/r times per
// lap — but INSIDE the track the two rotations subtract instead of adding, which is why
// the sign is minus and why the curve bites inward into cusps instead of bulging out into
// lobes.
//
// manic examples/hypocycloid.manic
title("A hypocycloid, and the straight line hiding inside it");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// ---- the track, the wheel inside it, and a pen on the wheel's rim -----------------
// `roll` does the mechanism. The wheel is placed INSIDE the track, so it rolls round the
// inside; the pen is an entity on its rim, and where you put it is the only difference
// between a hypocycloid and the hypotrochoids in examples/hypotrochoid.manic.
circle(track, (540, 760), 230);
outlined(track); outline(track, dim); stroke(track, 3);
circle(locus, (540, 760), 138); // R - r, the wheel centre's own circle
outlined(locus); outline(locus, dim); stroke(locus, 2); dashed(locus); opacity(locus, 0.45);
circle(wheel, (678, 760), 92);
outlined(wheel); outline(wheel, fg); stroke(wheel, 3); opacity(wheel, 0.8);
circle(pen, (770, 760), 9); color(pen, gold); // on the rim: cusps
line(arm, wheel, pen); color(arm, dim); opacity(arm, 0.55);
tag(wheel, rig); tag(pen, rig); tag(arm, rig);
trail(trace, pen, cyan, 4); // the curve draws itself
// ---- what the RATIO does: the same formula, three times ----------------------------
// written out in full rather than simplified, so the third one's collapse is visible
param(astro, (196, 1380), 110, 110, "0.75*cos(t) + 0.25*cos(3*t)", "0.75*sin(t) - 0.25*sin(3*t)", (0, 6.283185));
param(delto, (540, 1380), 110, 110, "0.667*cos(t) + 0.333*cos(2*t)", "0.667*sin(t) - 0.333*sin(2*t)", (0, 6.283185));
param(tusi, (884, 1380), 110, 110, "0.5*cos(t) + 0.5*cos(t)", "0.5*sin(t) - 0.5*sin(t)", (0, 6.283185));
gradient(astro, mint, cyan, blue);
gradient(delto, gold, coral, red);
gradient(tusi, magenta, violet, magenta);
stroke(astro, 3);
stroke(delto, 3);
stroke(tusi, 4);
untraced(astro);
untraced(delto);
untraced(tusi);
// ---- textbook annotations ----
// The wheel never stops. The curve shuts after two laps and the trace holds there, but
// the mechanism keeps turning for the rest of the scene, running the pen back around its
// own line — which is what closure looks like when you do not take the machine away.
// nothing but the mark, the title and the formula — the rolling does the explaining
caption(head, "A hypocycloid, and the straight line hiding inside it", (540, 130), 27);
equation(eq, (540, 1600),
`x=(R{-}r)\cos\theta+r\cos\tfrac{R-r}{r}\theta,\quad y=(R{-}r)\sin\theta-r\sin\tfrac{R-r}{r}\theta`, 26);
// the roll runs from t = 0; the three ratios draw once it has closed
roll(rig, track, 2, 9, linear); // R/r = 5/2 needs two laps
wait(0.4);
draw(astro, 1.6);
draw(delto, 1.6);
draw(tusi, 1.6);
wait(9);
epitrochoid
The curve a pen draws when it is bolted to a wheel rolling around the OUTSIDE of a fixed circle —
every spirograph rose is one of these. Three numbers: R the fixed circle, r the wheel, d how far the
pen sits from the wheel’s centre. The mechanism is not a drawing OF a mechanism: wheel, arm and pen
are evaluated from the same θ as the curve, so the pen is literally the leading end of the trace and
they cannot drift apart. Rolling without slipping forces the second angle — (R+r)/r turns of spin per
lap — which is why the pen carries two rotations and the curve has lobes at all. And it closes on
cue: write R/r in lowest terms as p/q and the pen comes home after q laps having drawn p lobes (here
5/2 — two laps, five lobes). The row below moves the pen in and out: d < r rounds off, d = r pulls
the loops into cusps and it is an epicycloid, d > r loops. param for the curve, cloud for the
moving parts.
// epitrochoid — the curve a pen draws when it is bolted to a wheel rolling around the
// OUTSIDE of a fixed circle. Every spirograph rose is one of these.
//
// x(θ) = (R+r)·cos θ - d·cos((R+r)/r · θ)
// y(θ) = (R+r)·sin θ - d·sin((R+r)/r · θ)
//
// Three numbers and nothing else: R the fixed circle, r the rolling wheel, d how far
// the pen sits from the wheel's centre. The name says which is which — a trochoid is
// the pen OFF the rim; put it exactly on the rim (d = r) and the loops pull tight into
// cusps and it is an epicycloid instead. That is the row along the bottom.
//
// The mechanism is not decoration and not a drawing of a mechanism: the wheel, the arm
// and the pen are all evaluated from the same θ as the curve, so the pen is literally
// the leading end of the trace. They cannot drift out of step, because there is nothing
// to drift — one formula, sampled twice.
//
// Rolling without slipping is the whole reason for the second angle. The wheel's centre
// goes round a circle of radius R+r, but the wheel itself has to turn through the arc it
// has travelled: (R+r)/r turns of spin per turn around. The pen therefore carries TWO
// rotations at once, which is what a single circle can never do and why the curve has
// lobes at all.
//
// It closes, and you can say exactly when. Write R/r in lowest terms as p/q: the pen
// comes home after q turns of θ, having drawn p lobes. The wheel spins 31 times doing it.
// Here R/r = 21/10, so it takes TEN laps to lay down 21 lobes — the pen crosses its own
// path all the way round and still does not join up until the tenth.
//
// The pace is deliberately uneven: the first two laps run at a steady walking speed,
// slow enough to watch the wheel actually roll and the pen swing, and the remaining
// eight wind up quadratically — `laps` is 2·min(t/slow, 1) + 8·min(max(t-slow, 0)/quick, 1)².
//
// Drawn with `param` for the finished curve (x(t), y(t) — the twin of `plot` for
// anything that is not y = f(x)) and `cloud` for the moving parts.
//
// manic examples/epitrochoid.manic
title("An epitrochoid, and the wheel that draws it");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// ---- the apparatus: fixed circle, and the circle the wheel's centre runs on --------
circle(fixed, (540, 800), 210);
outlined(fixed);
color(fixed, dim);
circle(locus, (540, 800), 310);
outlined(locus);
dashed(locus);
color(locus, dim);
opacity(locus, 0.45);
// ---- the trace: θ runs to 4π (two laps) and stops there ----------------------------
cloud(trace, 17000, #ffffff, 1.0) {
let slow = 6; // two laps at a walking pace…
let quick = 6; // …then eight, winding up
let a1 = min(t/slow, 1);
let b1 = min(max(t - slow, 0)/quick, 1);
let laps = 2*a1 + 8*b1*b1; // 2 steady, then 8 accelerating
let th = 6.283185*laps*(i/17000); // R/r = 21/10 needs TEN laps
let x = 540 + 100*(3.1*cos(th) - 1.4*cos(3.1*th));
let y = 800 - 100*(3.1*sin(th) - 1.4*sin(3.1*th));
let hue = mod(28 + th*11, 360); // hue rides θ: every lap its own
let sat = 0.72;
let r = 1.7;
}
// ---- the wheel, its arm, and the pen — same θ, so they cannot drift ----------------
cloud(wheel, 300, #ffffff, 1.0) {
let slow = 6; // two laps at a walking pace…
let quick = 6; // …then eight, winding up
let a1 = min(t/slow, 1);
let b1 = min(max(t - slow, 0)/quick, 1);
let laps = 2*a1 + 8*b1*b1; // 2 steady, then 8 accelerating
let th = 6.283185*laps;
let spoke = i/300*6.283185;
let x = 540 + 100*(3.1*cos(th) + cos(spoke)); // centre at R+r, radius r
let y = 800 - 100*(3.1*sin(th) + sin(spoke));
let sat = 0;
let r = 1.5;
let alpha = 0.8;
}
cloud(arm, 110, #ffffff, 1.0) {
let slow = 6; // two laps at a walking pace…
let quick = 6; // …then eight, winding up
let a1 = min(t/slow, 1);
let b1 = min(max(t - slow, 0)/quick, 1);
let laps = 2*a1 + 8*b1*b1; // 2 steady, then 8 accelerating
let th = 6.283185*laps;
let v = i/110; // centre -> pen, in a straight line
let cx = 3.1*cos(th);
let cy = 3.1*sin(th);
let px = cx - 1.4*cos(3.1*th);
let py = cy - 1.4*sin(3.1*th);
let x = 540 + 100*(cx + v*(px - cx));
let y = 800 - 100*(cy + v*(py - cy));
let sat = 0;
let r = 1.3;
let alpha = 0.55;
}
cloud(pen, 90, #ffffff, 1.0) {
let slow = 6; // two laps at a walking pace…
let quick = 6; // …then eight, winding up
let a1 = min(t/slow, 1);
let b1 = min(max(t - slow, 0)/quick, 1);
let laps = 2*a1 + 8*b1*b1; // 2 steady, then 8 accelerating
let th = 6.283185*laps;
let nib = i/90*6.283185;
let rr = 7*(i/90); // a small filled nib, not a ring
let x = 540 + 100*(3.1*cos(th) - 1.4*cos(3.1*th)) + rr*cos(nib*9);
let y = 800 - 100*(3.1*sin(th) - 1.4*sin(3.1*th)) + rr*sin(nib*9);
let hue = 45;
let sat = 0.5;
let r = 1.8;
}
// ---- what d does: the same R and r, the pen moved in and out -----------------------
param(curtate, (196, 1452), 30, 30, "3.1*cos(t) - 0.6*cos(3.1*t)", "3.1*sin(t) - 0.6*sin(3.1*t)", (0, 62.831853));
param(cusped, (540, 1452), 30, 30, "3.1*cos(t) - cos(3.1*t)", "3.1*sin(t) - sin(3.1*t)", (0, 62.831853));
param(looped, (884, 1452), 30, 30, "3.1*cos(t) - 1.4*cos(3.1*t)", "3.1*sin(t) - 1.4*sin(3.1*t)", (0, 62.831853));
gradient(curtate, mint, cyan, blue); // hue rides ARC LENGTH along the
gradient(cusped, gold, coral, red); // stroke, the way the big trace
gradient(looped, magenta, violet, indigo); // rides θ — same idea, one curve
stroke(curtate, 3);
stroke(cusped, 3);
stroke(looped, 3);
untraced(curtate);
untraced(cusped);
untraced(looped);
caption(lc, "d < r", (196, 1608), 20);
caption(lm, "d = r (an epicycloid)", (540, 1608), 20);
caption(lr, "d > r", (884, 1608), 20);
hidden(lc);
hidden(lm);
hidden(lr);
// ---- textbook annotations ----
caption(head, "An epitrochoid, and the wheel that draws it", (540, 140), 30);
caption(sub, "a pen bolted to a wheel rolling on the outside of a circle", (540, 200), 20);
hidden(sub);
equation(eq, (540, 1690),
`x=(R+r)\cos\theta-d\cos\tfrac{R+r}{r}\theta,\quad y=(R+r)\sin\theta-d\sin\tfrac{R+r}{r}\theta`, 23);
caption(spin, "the wheel must spin (R+r)/r times per lap — rolling, not sliding", (540, 1768), 19);
caption(shut, "R/r = 21/10 in lowest terms: ten laps to shut, and 21 lobes", (540, 1818), 19);
hidden(eq);
hidden(spin);
hidden(shut);
wait(1.6);
show(sub);
wait(2.2);
show(spin);
wait(4.2);
show(eq);
wait(4.4);
show(shut);
wait(1.6);
draw(curtate, 1.4);
draw(cusped, 1.4);
draw(looped, 1.4);
show(lc);
show(lm);
show(lr);
wait(9);
epitrochoid
The curve a pen draws when it is bolted to a wheel rolling around the OUTSIDE of a fixed circle —
every spirograph rose is one of these. Three numbers: R the fixed circle, r the wheel, d how far the
pen sits from the wheel’s centre. The mechanism is not a drawing OF a mechanism: wheel, arm and pen
are evaluated from the same θ as the curve, so the pen is literally the leading end of the trace and
they cannot drift apart. Rolling without slipping forces the second angle — (R+r)/r turns of spin per
lap — which is why the pen carries two rotations and the curve has lobes at all. And it closes on cue:
write R/r in lowest terms as p/q and the pen comes home after q laps having drawn p lobes — here 21/10,
so it crosses its own path for TEN laps and 31 turns of the wheel before it shuts, on 21 lobes. The row
below moves the pen in and out at the same ratio: d < r rounds off, d = r pulls the loops
into cusps and it is an epicycloid, d > r loops. param for the curves, cloud for the moving parts.
// epitrochoid — the curve a pen draws when it is bolted to a wheel rolling around the
// OUTSIDE of a fixed circle. Every spirograph rose is one of these.
//
// x(θ) = (R+r)·cos θ - d·cos((R+r)/r · θ)
// y(θ) = (R+r)·sin θ - d·sin((R+r)/r · θ)
//
// Three numbers and nothing else: R the fixed circle, r the rolling wheel, d how far
// the pen sits from the wheel's centre. The name says which is which — a trochoid is
// the pen OFF the rim; put it exactly on the rim (d = r) and the loops pull tight into
// cusps and it is an epicycloid instead. That is the row along the bottom.
//
// The mechanism is not decoration and not a drawing of a mechanism: the wheel, the arm
// and the pen are all evaluated from the same θ as the curve, so the pen is literally
// the leading end of the trace. They cannot drift out of step, because there is nothing
// to drift — one formula, sampled twice.
//
// Rolling without slipping is the whole reason for the second angle. The wheel's centre
// goes round a circle of radius R+r, but the wheel itself has to turn through the arc it
// has travelled: (R+r)/r turns of spin per turn around. The pen therefore carries TWO
// rotations at once, which is what a single circle can never do and why the curve has
// lobes at all.
//
// It closes, and you can say exactly when. Write R/r in lowest terms as p/q: the pen
// comes home after q turns of θ, having drawn p lobes. The wheel spins 31 times doing it.
// Here R/r = 21/10, so it takes TEN laps to lay down 21 lobes — the pen crosses its own
// path all the way round and still does not join up until the tenth.
//
// The pace is deliberately uneven: the first two laps run at a steady walking speed,
// slow enough to watch the wheel actually roll and the pen swing, and the remaining
// eight wind up quadratically — `laps` is 2·min(t/slow, 1) + 8·min(max(t-slow, 0)/quick, 1)².
//
// Drawn with `param` for the finished curve (x(t), y(t) — the twin of `plot` for
// anything that is not y = f(x)) and `cloud` for the moving parts.
//
// manic examples/epitrochoid.manic
title("An epitrochoid, and the wheel that draws it");
canvas("9:16");
template("black");
bloom(0.3, 0.62, 22);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// ---- the apparatus: fixed circle, and the circle the wheel's centre runs on --------
circle(fixed, (540, 800), 210);
outlined(fixed);
color(fixed, dim);
circle(locus, (540, 800), 310);
outlined(locus);
dashed(locus);
color(locus, dim);
opacity(locus, 0.45);
// ---- the trace: θ runs to 4π (two laps) and stops there ----------------------------
cloud(trace, 17000, #ffffff, 1.0) {
let slow = 6; // two laps at a walking pace…
let quick = 6; // …then eight, winding up
let a1 = min(t/slow, 1);
let b1 = min(max(t - slow, 0)/quick, 1);
let laps = 2*a1 + 8*b1*b1; // 2 steady, then 8 accelerating
let th = 6.283185*laps*(i/17000); // R/r = 21/10 needs TEN laps
let x = 540 + 100*(3.1*cos(th) - 1.4*cos(3.1*th));
let y = 800 - 100*(3.1*sin(th) - 1.4*sin(3.1*th));
let hue = mod(28 + th*11, 360); // hue rides θ: every lap its own
let sat = 0.72;
let r = 1.7;
}
// ---- the wheel, its arm, and the pen — same θ, so they cannot drift ----------------
cloud(wheel, 300, #ffffff, 1.0) {
let slow = 6; // two laps at a walking pace…
let quick = 6; // …then eight, winding up
let a1 = min(t/slow, 1);
let b1 = min(max(t - slow, 0)/quick, 1);
let laps = 2*a1 + 8*b1*b1; // 2 steady, then 8 accelerating
let th = 6.283185*laps;
let spoke = i/300*6.283185;
let x = 540 + 100*(3.1*cos(th) + cos(spoke)); // centre at R+r, radius r
let y = 800 - 100*(3.1*sin(th) + sin(spoke));
let sat = 0;
let r = 1.5;
let alpha = 0.8;
}
cloud(arm, 110, #ffffff, 1.0) {
let slow = 6; // two laps at a walking pace…
let quick = 6; // …then eight, winding up
let a1 = min(t/slow, 1);
let b1 = min(max(t - slow, 0)/quick, 1);
let laps = 2*a1 + 8*b1*b1; // 2 steady, then 8 accelerating
let th = 6.283185*laps;
let v = i/110; // centre -> pen, in a straight line
let cx = 3.1*cos(th);
let cy = 3.1*sin(th);
let px = cx - 1.4*cos(3.1*th);
let py = cy - 1.4*sin(3.1*th);
let x = 540 + 100*(cx + v*(px - cx));
let y = 800 - 100*(cy + v*(py - cy));
let sat = 0;
let r = 1.3;
let alpha = 0.55;
}
cloud(pen, 90, #ffffff, 1.0) {
let slow = 6; // two laps at a walking pace…
let quick = 6; // …then eight, winding up
let a1 = min(t/slow, 1);
let b1 = min(max(t - slow, 0)/quick, 1);
let laps = 2*a1 + 8*b1*b1; // 2 steady, then 8 accelerating
let th = 6.283185*laps;
let nib = i/90*6.283185;
let rr = 7*(i/90); // a small filled nib, not a ring
let x = 540 + 100*(3.1*cos(th) - 1.4*cos(3.1*th)) + rr*cos(nib*9);
let y = 800 - 100*(3.1*sin(th) - 1.4*sin(3.1*th)) + rr*sin(nib*9);
let hue = 45;
let sat = 0.5;
let r = 1.8;
}
// ---- what d does: the same R and r, the pen moved in and out -----------------------
param(curtate, (196, 1452), 30, 30, "3.1*cos(t) - 0.6*cos(3.1*t)", "3.1*sin(t) - 0.6*sin(3.1*t)", (0, 62.831853));
param(cusped, (540, 1452), 30, 30, "3.1*cos(t) - cos(3.1*t)", "3.1*sin(t) - sin(3.1*t)", (0, 62.831853));
param(looped, (884, 1452), 30, 30, "3.1*cos(t) - 1.4*cos(3.1*t)", "3.1*sin(t) - 1.4*sin(3.1*t)", (0, 62.831853));
gradient(curtate, mint, cyan, blue); // hue rides ARC LENGTH along the
gradient(cusped, gold, coral, red); // stroke, the way the big trace
gradient(looped, magenta, violet, indigo); // rides θ — same idea, one curve
stroke(curtate, 3);
stroke(cusped, 3);
stroke(looped, 3);
untraced(curtate);
untraced(cusped);
untraced(looped);
caption(lc, "d < r", (196, 1608), 20);
caption(lm, "d = r (an epicycloid)", (540, 1608), 20);
caption(lr, "d > r", (884, 1608), 20);
hidden(lc);
hidden(lm);
hidden(lr);
// ---- textbook annotations ----
caption(head, "An epitrochoid, and the wheel that draws it", (540, 140), 30);
caption(sub, "a pen bolted to a wheel rolling on the outside of a circle", (540, 200), 20);
hidden(sub);
equation(eq, (540, 1690),
`x=(R+r)\cos\theta-d\cos\tfrac{R+r}{r}\theta,\quad y=(R+r)\sin\theta-d\sin\tfrac{R+r}{r}\theta`, 23);
caption(spin, "the wheel must spin (R+r)/r times per lap — rolling, not sliding", (540, 1768), 19);
caption(shut, "R/r = 21/10 in lowest terms: ten laps to shut, and 21 lobes", (540, 1818), 19);
hidden(eq);
hidden(spin);
hidden(shut);
wait(1.6);
show(sub);
wait(2.2);
show(spin);
wait(4.2);
show(eq);
wait(4.4);
show(shut);
wait(1.6);
draw(curtate, 1.4);
draw(cusped, 1.4);
draw(looped, 1.4);
show(lc);
show(lm);
show(lr);
wait(9);
cloud-cat-map
A dwitter one-liner whose punchline is hiding in its constants. It shoves cat emoji through a
shearing lattice; work the numbers out and the basis is 100 A^(t/3) for A = [[2,1],[1,1]] —
Arnold’s cat map, and 2.62/44.7/117/17 are φ², 100/√5, 100φ²/√5, 100/(φ²√5). Not a slideshow of
iterations: a matrix raised to a FRACTIONAL power, flowing from I to A over three seconds. Two
facts get staged. det A = 1 with whole entries, so the sheared lattice IS the lattice — the loop
is seamless because of a determinant, not a crossfade. And the glyph is chosen by q = i² - ij - j²,
the norm form of Z[φ], which A leaves invariant — so each cat returns as the SAME cat, not just to
the same place; here that invariant is the colour, four hues because q² mod 9 is only ever 0, 1, 4
or 7. One cloud per body part (head, ears, eyes) over a shared lattice, cats upright as it shears.
// cloud-cat-map — a dwitter-sized canvas one-liner, reimagined. The original golf is
// 20 columns of cat emoji shoved through a shearing lattice:
// e = 2.62**(t%3/3), k = 44.7*(e - 1/e), a = 117/e - 17*e + k
// for (i = 20; i-- > -20;) for (j = -20; j++ < 20;)
// fillText( catFace(q*q % 9), 928 + i*(k+a) + j*k, 565 + i*k + j*a ), q = i*i - i*j - j*j
//
// The cats are not decoration — they are the joke, and it took working the constants out
// to see it. Every point sits at `i·(k+a, k) + j·(k, a)`, so the lattice basis is the
// matrix [[k+a, k], [k, a]]. Feed it e = 1 (the start of each cycle) and that is 100·I.
// Feed it e = 2.62 (the end) and it is
//
// [ 200 100 ] [ 2 1 ]
// [ 100 100 ] = 100· [ 1 1 ] ← Arnold's CAT MAP
//
// and the four magic constants are the golden ratio wearing a hat: 2.62 = φ², 44.7 =
// 100/√5, 117 = 100φ²/√5, 17 = 100/(φ²√5). Put those back and the basis is exactly
// 100·A^(t/3) — not a slideshow of iterations, but the matrix itself raised to a
// FRACTIONAL power, flowing continuously from I to A over three seconds. (Written out:
// A^s = U(s)·A - U(s-1)·I with U(s) = sinh(s ln φ²)/sinh(ln φ²), which is where `e - 1/e`
// comes from.) We use the exact constants rather than the rounded ones, so the loop
// closes on the number as well as on the eye.
//
// Two things follow, and this staging is built to show both:
//
// · det A = 1 and every entry is a whole number, so A maps the integer lattice ONTO
// itself. At t = 3 every cat has moved, and the set of places cats are is the set it
// was at t = 0. The loop is seamless because of a determinant, not because of a
// crossfade.
// · the glyph is picked by q = i² - ij - j², the norm form of the golden ring Z[φ] —
// and q(2i+j, i+j) = q(i,j), so A leaves it ALONE. Each cat therefore comes back as
// the same cat, not merely to the same place. Here that invariant is the colour:
// four hues, because q² mod 9 only ever takes the values 0, 1, 4 and 7.
//
// So it is one `cloud` per body part over a shared lattice: head, ears (two straight
// segments folded branchlessly into one index, because a cosine bump can round a corner
// but never point an ear), eyes. The cats stay upright as the lattice shears under them,
// exactly as `fillText` keeps its glyphs upright — the deformation is where they ARE,
// not what they are.
//
// The index box runs wider than the frame (i ±8, j ±13) because a sheared lattice
// reaches: at t = 3 the cat that fills a corner started thirteen cells away.
//
// Pure in (i, t) — it scrubs, seeks and records; the canvas original can only play.
//
// manic examples/cloud-cat-map.manic
title("Arnold's cat map, made of cats");
canvas("9:16");
template("black");
bloom(0.26, 0.6, 20);
text(brand, (540, 34), "maniclang.com");
display(brand);
size(brand, 21);
color(brand, fg);
opacity(brand, 0.82);
// 17 x 27 = 459 cells; the cat that is on screen now was off screen a second ago.
cloud(heads, 14688, #ffffff, 1.0) {
let per = 32;
let cell = (i - mod(i, per))/per;
let ci = mod(cell, 17) - 8;
let cj = (cell - mod(cell, 17))/17 - 13;
let s = mod(t, 3)/3;
let e = 2.6180339887^s; // φ^(2t/3): the unstable eigenvalue
let k = 44.7213595*(e - 1/e);
let a = 117.0820393/e - 17.0820393*e + k;
let cx = 540 + 2*(ci*(k + a) + cj*k); // basis columns (k+a, k) and (k, a)
let cy = 960 + 2*(ci*k + cj*a);
let th = mod(i, per)/per*6.28319;
let x = cx + 62*cos(th)*1.05;
let y = cy - 62*sin(th)*0.92;
let q = ci*ci - ci*cj - cj*cj; // invariant under A — so is the hue
let hue = mod(18 + mod(q*q, 9)*44, 360);
let sat = 0.62;
let r = 1.7;
}
cloud(ears, 10098, #ffffff, 1.0) {
let per = 22;
let cell = (i - mod(i, per))/per;
let ci = mod(cell, 17) - 8;
let cj = (cell - mod(cell, 17))/17 - 13;
let s = mod(t, 3)/3;
let e = 2.6180339887^s;
let k = 44.7213595*(e - 1/e);
let a = 117.0820393/e - 17.0820393*e + k;
let cx = 540 + 2*(ci*(k + a) + cj*k);
let cy = 960 + 2*(ci*k + cj*a);
let loc = mod(i, per);
let sd = sign(loc - 10.5); // which ear
let u = mod(loc, 11)/11;
let w = (1 + sign(u - 0.5))/2; // 0 = base->apex, 1 = apex->base:
let v = mod(u, 0.5)*2; // two segments, no branch
let bx = sd*0.22*62; let by = 0.94*62;
let ax = sd*0.66*62; let ay = 1.34*62;
let ex = sd*0.92*62; let ey = 0.58*62;
let sx = bx + w*(ax - bx); let sy = by + w*(ay - by);
let tx = ax + w*(ex - ax); let ty = ay + w*(ey - ay);
let x = cx + (sx + v*(tx - sx))*1.05;
let y = cy - (sy + v*(ty - sy))*0.92;
let q = ci*ci - ci*cj - cj*cj;
let hue = mod(18 + mod(q*q, 9)*44, 360);
let sat = 0.62;
let r = 1.7;
}
cloud(eyes, 4590, #ffffff, 1.0) {
let per = 10;
let cell = (i - mod(i, per))/per;
let ci = mod(cell, 17) - 8;
let cj = (cell - mod(cell, 17))/17 - 13;
let s = mod(t, 3)/3;
let e = 2.6180339887^s;
let k = 44.7213595*(e - 1/e);
let a = 117.0820393/e - 17.0820393*e + k;
let cx = 540 + 2*(ci*(k + a) + cj*k);
let cy = 960 + 2*(ci*k + cj*a);
let loc = mod(i, per);
let sd = sign(loc - 4.5);
let u = mod(loc, 5)/5*6.28319;
let x = cx + (sd*0.34*62 + 0.13*62*cos(u))*1.05;
let y = cy - (0.10*62 + 0.13*62*sin(u))*0.92;
let q = ci*ci - ci*cj - cj*cj;
let hue = mod(18 + mod(q*q, 9)*44, 360);
let sat = 0.45;
let r = 1.7;
}
// ---- textbook annotations ----
caption(head, "Arnold's cat map, made of cats", (540, 138), 34);
caption(sub, "every 3 seconds the lattice lands on itself", (540, 206), 22);
plate(head, 0.72); // the field goes all the way to the
plate(sub, 0.72); // edges, so every label needs one
hidden(sub);
equation(eq, (540, 1652), `A=\begin{pmatrix}2&1\\1&1\end{pmatrix},\quad \det A = 1`, 30);
caption(lab, "the frame is A raised to the power t/3 — a matrix, flowed", (540, 1730), 20);
caption(inv, "colour is q = i^2 - ij - j^2, and A leaves q alone:", (540, 1778), 20);
caption(inv2, "each cat returns as the same cat, not just to the same place", (540, 1822), 20);
plate(eq, 0.72);
plate(lab, 0.72);
plate(inv, 0.72);
plate(inv2, 0.72);
plate(brand, 0.6);
hidden(eq);
hidden(lab);
hidden(inv);
hidden(inv2);
wait(1.6);
show(sub);
wait(2.8);
show(eq);
show(lab);
wait(1.4);
show(inv);
show(inv2);
wait(21);
glsl-medusae-glow
Two ‘one formula’ pieces sharing a frame: a raw glsl glow field draws bioluminescent medusae as
pure inverse-square LIGHT traced along an epicycle (no geometry — every pixel just sums its distance
to the glowing thread), with an HDR tone-map so the cores bloom instead of clipping; above it, the
koi cloud swims. A particle system and a per-pixel field on one stage, both pure in (i, t).
// glsl-medusae-glow — four bioluminescent medusae drawn as pure LIGHT. Each is a
// two-frequency (epicyclic) orbit; we trace 55 time-lagged beads along it and add
// inverse-square glow, so the filament reads as a bright creature trailing into the
// dark. No geometry, no particles — every pixel just asks "how close am I to each
// glowing thread?" and sums the light. Runs on manic's raw glsl() path (Shadertoy
// mainImage, iTime/iResolution) — pure in (pixel, time), so it scrubs and records.
//
// Our take on the classic compact glow-medusae shader: the epicycle + inverse-square
// core is kept exactly; the elevation is honest post — a faint deep-sea backing and a
// tone-map so the cores bloom softly instead of clipping to flat white.
//
// manic examples/glsl-medusae-glow.manic
title("Koi through a field of living light");
canvas("16:9");
template("black");
glsl(field, `
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 r = iResolution.xy;
float t = iTime;
// aspect-correct coords, centre pushed to the LOWER third so the koi above
// has clear space (y runs about -0.66 .. +0.34 top to bottom here)
vec2 u = (fragCoord - vec2(0.5, 0.34) * r) / r.y;
vec3 col = vec3(0.0);
// four medusae — each its own hue, speed, and inner/outer frequency ratio
for (float j = 0.0; j < 4.0; j++) {
float s = 1.0 + j; // base angular speed
float k = 2.0 + j; // small-loop frequency ratio
vec3 h = 0.5 + 0.5 * cos(j * 9.0 + vec3(1.0, 2.0, 3.0)); // per-medusa hue
// nearest squared distance from this pixel to the glowing filament
float d = 1e9;
for (float i = 0.0; i < 55.0; i++) {
float e = t * 0.5 - i * 0.03; // time-lagged phase down the trail
vec2 b = vec2(cos(e * s), sin(e * s)) * 0.10 // large epicycle
+ vec2(cos(e * s * k), sin(e * s * k)) * 0.04; // small epicycle
d = min(d, dot(u - b, u - b));
}
// inverse-square light: a bright thread with a soft, wide halo
col += h * 3e-5 / (d + 5e-9);
}
// --- our elevation: honest post, same field underneath ---
col += vec3(0.010, 0.028, 0.060) * (1.0 - length(u) * 0.6); // deep-sea backing
col = 1.0 - exp(-col * 1.2); // HDR bloom, cores don't clip
col = pow(col, vec3(0.85)); // gentle gamma lift
fragColor = vec4(col, 1.0);
}
`);
// ---- the koi: 10,000 points in two layers (mod i,2) swirl into koi chasing each
// other, from cloud-koi. A polar plot (radius q, angle c). Same formula as the
// standalone piece — only recentred + rescaled for this 16:9 stage — so the fish
// swim THROUGH the glow field above. Pure in (i, t); it scrubs and records.
cloud(koi, 10000, #ffffff, 0.6) {
let s = i/790;
let m = 0; // one koi is enough (was mod i,2 = two)
let sw = 0.5*(1 + sign(8 - s)); // (y<8 ? … : …)
let scr = noise(s*2, 0); // ~ the JS y^9 XOR scramble
let kbase = sw*(9 + scr*6) + (1 - sw)*(4 + cos(s));
let k = kbase * cos(i + t/4);
let e = s/3 - 13;
let d = hypot(k, e) + cos(e + t*2 + m*4);
let q = s*k/5*(2 + sin(d*2 + s - t*4)) + 80;
let c = d/4 - t/2 + m*3;
let px = q*cos(c);
let py = q*sin(c) + d*9 - 130; // recentre the d*9+60 offset
let grow = tanh(t*0.5 + 0.12);
let x = 640 + px * 1.3 * grow; // 16:9 stage: koi in the UPPER half…
let y = 285 + py * 1.3 * grow; // …with the glow core sitting clear in the lower third
let hue = mod(m*90 + i*0.03 + t*14, 360);
}
// ---- textbook annotations (kept off the glow, backed for legibility) ----
caption(head, "Koi through a field of living light", (640, 66), 30);
plate(head);
caption(sub, "two formulas, no simulation — a glow field and a koi cloud", (640, 112), 20);
plate(sub);
hidden(head);
hidden(sub);
equation(eq, (640, 636), `b(e)=0.1\,(\cos es,\sin es)+0.04\,(\cos esk,\sin esk)`, 26);
plate(eq);
caption(lab, "medusae: light traced along an epicycle · koi: 10,000 polar points", (640, 682), 18);
plate(lab);
hidden(eq);
hidden(lab);
// let the field breathe, then bring the story in over it
wait(2.0);
show(head);
wait(1.6);
show(sub);
wait(3.0);
show(eq);
show(lab);
wait(18);
glsl-fractal-nebula
A twigl-style 3D fractal fold as a soft gold dawn with blue frost-ferns (raw glsl, accumulated
into a LOCAL vec3 — the o.rgb += in-loop idiom miscompiles on Metal), and a murmuration cloud
sweeping across it: a per-pixel field and a particle system sharing one frame, both pure in (i, t).
// glsl-fractal-nebula — a twigl-style 3D fractal fold as a soft gold dawn field
// with blue frost-ferns (raw `glsl`, accumulated into a LOCAL vec3 — the `o.rgb +=`
// in-loop idiom miscompiles on the Metal backend), and a murmuration `cloud`
// sweeping across it: a per-pixel field and a particle system sharing one frame.
// Both pure in (pixel/i, t), so the whole scene scrubs and records.
//
// manic examples/glsl-fractal-nebula.manic
title("A fractal dawn, and a murmuration");
canvas("16:9");
template("black");
glsl(scene, `
mat3 rotate3D(float angle, vec3 axis){
axis = normalize(axis);
float s = sin(angle), c = cos(angle), oc = 1.0 - c;
return mat3(
oc*axis.x*axis.x + c, oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s,
oc*axis.x*axis.y + axis.z*s, oc*axis.y*axis.y + c, oc*axis.y*axis.z - axis.x*s,
oc*axis.z*axis.x - axis.y*s, oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c
);
}
vec3 hsv(float h, float s, float v){
vec3 rgb = clamp(abs(mod(h*6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
return v * mix(vec3(1.0), rgb, s);
}
void mainImage(out vec4 o, in vec2 FC){
vec2 r = iResolution.xy;
float t = iTime;
vec3 col = vec3(0.0); // accumulate here, not into o
float i = 0., g = 0., e = 0., s = 0.;
for(int n = 0; n < 98; n++){
i += 1.0;
vec3 p = vec3((FC.xy-.5*r)/r.y*5. + vec2(0,9), g)
* rotate3D(-1.1 - cos(t*.15)*.1, vec3(1, 11.+sin(t)*.15, -1.5));
s = 2.;
for(int j = 0; j < 19; j++){
s *= e = 7.1/dot(p, p*.51);
p = vec3(.08,4,-1) - abs(abs(p)*e - vec3(3,4,3));
}
g += p.y/s;
s = log2(s)/exp(e);
col += .01 - hsv(.1, g*.016 - e*.3, s/2e2); // original's o.rgb += …, into the local
}
o = vec4(col, 1.0);
}
`);
// a murmuration sweeping across the still dawn field — a cohesive blob of birds
// (golden-angle scatter, denser core) whose centre sweeps a path, stretched along
// motion and banked into each turn, breathing organically. Pure in (i, t).
cloud(flock, 9000, #e8f6ff, 0.8) {
let s = i/9000; // 0..1 through the flock
let ang = i*2.39996; // golden-angle scatter
let rad = sqrt(s); // wispy toward the edge
let sw = ang + rad*3*sin(t*0.5) + t*0.6; // the interior swirls (shape-shifting)
let taper = 1 - 0.45*s; // tail thins out
let ex = rad*cos(sw)*235*taper; // elongated along motion…
let ey = rad*sin(sw)*88*taper; // …narrower across
let turb = 70*rad*rad; // tendrils: turbulence grows at the edge
let bx = ex + turb*sin(i*0.7 + t*2.2);
let by = ey + turb*cos(i*0.9 + t*1.9);
let phase = t*0.45;
let cx = 640 + 330*sin(phase); // the flock sweeps left↔right…
let cy = 250 + 80*sin(phase*1.6 + 0.7); // …rising and dipping
let bank = 0.6*cos(phase); // and banks into each turn
let rx = bx*cos(bank) - by*sin(bank);
let ry = bx*sin(bank) + by*cos(bank);
let grow = tanh(t*0.6 + 0.1);
let x = cx + rx*grow;
let y = cy + ry*grow;
let hue = mod(210 + s*14 + t*5, 360);
}
wait(12);
cloud-feathers
Another @yuruyurau art-tweet in ONE cloud: 20,000 points in FOUR layers (mod(i,4)) fan into
feathery plumes that flutter — a polar plot whose amplitude switches on a conditional (the p5
ternary becomes a sign() blend, since cloud formulas have none). Hue-coloured, bloomed, 9:16.
// cloud-feathers — another @yuruyurau art-tweet in ONE `cloud`: 20,000 points in
// FOUR layers (`mod(i,4)`) fan into feathery plumes that flutter over time. A
// polar plot (radius `q`, angle `c`) whose amplitude switches on a conditional —
// the original's `(y<5 ? … : 11)` becomes a `sign()` blend, since cloud formulas
// have no ternary. `mag(k,e)` here (not squared); coloured per point and bloomed
// on a 9:16 Short. The parameter is renamed `s` (the required output is `y`).
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated
// take — pure in (i, t), so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-feathers.manic
title("Four feathers from one formula");
canvas("9:16");
template("black");
cloud(feathers, 20000, #ffffff, 0.6) {
let s = i/500; // reference's "y"
let m = mod(i, 4) * 8; // four layers
let sw = 0.5*(1 + sign(5 - s)); // (y<5 ? … : …) via sign
let kamp = sw*(sin(t/8 + s)*35) + (1 - sw)*11;
let k = cos(s*9) * kamp;
let e = s/8 - 13;
let o = hypot(k, e)/6;
let q = k*s/19 + 49 + k*sin(s)*sin(o*2 - e/5 - t);
let c = o/3 - e/5 - t/8 + m;
let px = q*sin(c) - 79*cos(c/3);
let py = (q + 70)*cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 1.9 * grow;
let y = 960 + py * 1.9 * grow;
let hue = mod(m*45 + i*0.04 + t*14, 360);
}
// ---- textbook annotations ----
caption(head, "Four feathers from one formula", (540, 138), 34);
caption(sub, "20,000 points, no simulation", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q{+}70)\cos c)`, 30);
caption(lab, "a polar plot in four layers (mod i,4)", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-jellyfish
Another @yuruyurau art-tweet in ONE cloud: 20,000 points in two layers (mod(i,2)) drift into
jellyfish-like bells with trailing tendrils and pulse over time — a polar plot with a nested
sin(sin(...)) ripple, hue-coloured and bloomed on a 9:16 Short. Pure in (i, t), so it scrubs.
// cloud-jellyfish — another @yuruyurau art-tweet in ONE `cloud`: 20,000 points in
// two layers (`mod(i,2)`) drift into jellyfish-like bells with trailing tendrils
// and pulse over time. A polar plot (radius `q`, angle `c`) with a nested
// `sin(sin(...))` that gives the bell its soft ripple; coloured per point and
// bloomed from the centre on a 9:16 Short. `mag(k,e)^2` → `k*k+e*e`; the
// parameter is renamed `s` (the required output is `y`).
//
// Original idea by @yuruyurau (https://x.com/yuruyurau). Our hue'd, annotated
// take — pure in (i, t), so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-jellyfish.manic
title("Two jellyfish from one formula");
canvas("9:16");
template("black");
cloud(jelly, 20000, #ffffff, 0.6) {
let s = i/99; // reference's "y" parameter
let m = mod(i, 2) * 3; // two layers
let k = 9*cos(s*2);
let e = s/8 - 12;
let d = (k*k + e*e)/79 + 1;
let q = 79 - e*sin(k) + k/d*(8 + 4*sin(sin(d*d + e/9 - t)));
let c = d/2 - cos(d*2)/5 - t/16 + m;
let px = q*sin(c);
let py = (q + 40)*cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.2 * grow;
let y = 960 + py * 2.2 * grow;
let hue = mod(m*70 + i*0.05 + t*15, 360);
}
// ---- textbook annotations ----
caption(head, "Two jellyfish from one formula", (540, 138), 34);
caption(sub, "20,000 points, no simulation", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q{+}40)\cos c)`, 30);
caption(lab, "a polar plot: radius q, angle c, per point", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-jellyfish-v2
The jellyfish cloud, now SWIMMING IN A SHADER AQUARIUM — a demo that a particle system and a
per-pixel field share one frame. The bells are the v1 cloud, re-lit into a cyan↔magenta
bioluminescent band; everything around them is one shader: a depth gradient (teal surface →
deep navy), animated caustics, and soft god-rays from the surface. Rising bubbles are a second
tiny cloud. Two clouds + one ocean shader + captions, all pure in (i, t) — the whole tank scrubs.
// cloud-jellyfish-v2 — the @yuruyurau jellyfish `cloud` (20,000 points, two layers,
// pulsing bells + tendrils), now SWIMMING IN A SHADER AQUARIUM. The bells are the same
// particle system as v1; everything around them is one per-pixel `shader`: a depth
// gradient (teal surface → deep navy), animated caustics rippling near the top, and soft
// god-rays falling from the surface. A third element — rising bubbles — is a second tiny
// `cloud`. Particle art + a per-pixel ocean + generic captions, all in one 9:16 frame,
// all pure in (i, t) so the whole aquarium scrubs and records exactly.
//
// manic examples/cloud-jellyfish-v2.manic
title("Jellyfish in a shader aquarium");
canvas("9:16");
template("black");
// ===================== the aquarium — one shader, per pixel =====================
shader(water) {
let x = (u - 0.5) * asp;
let y = v; // 0 = surface (top), 1 = deep (bottom)
let depth = smoothstep(0.0, 1.0, y);
// deep-water colour ramp: bright teal near the surface, deep blue below
let hue = mix(186, 216, depth);
let base = mix(0.26, 0.045, depth);
// caustics — warped interference, bright veins that fade with depth
let wx = x * 4.0 + 0.5 * sin(y * 6.0 + t * 0.4);
let wy = y * 7.0 + 0.5 * sin(x * 5.0 - t * 0.5);
let cc = sin(wx + t * 0.7) + sin(wy - t * 0.6) + sin((wx + wy) * 0.7 + t * 0.5);
let b = 0.5 + 0.5 * sin(cc * 1.5);
let caust = b * b * b * (1.0 - depth * 0.75);
// god-rays — soft vertical light shafts from the surface, strongest up top
let ray = 0.5 + 0.5 * sin(x * 3.0 + 0.6 * sin(t * 0.2));
let r2 = ray * ray;
let rays = r2 * r2 * (1.0 - smoothstep(0.0, 0.65, y)) * 0.45;
let val = clamp(base + caust * 0.5 + rays, 0.0, 0.95);
let sat = mix(0.85, 0.62, caust); // bright veins read a touch whiter
}
// ===================== the jellyfish — the v1 cloud, re-lit ====================
// same polar formula as v1; the hue is pulled into a cyan↔magenta bioluminescent band
// so the bells glow like sea creatures against the water instead of full-spectrum.
cloud(jelly, 20000, #ffffff, 0.6) {
let s = i/99; // reference's "y" parameter
let m = mod(i, 2) * 3; // two layers
let k = 9*cos(s*2);
let e = s/8 - 12;
let d = (k*k + e*e)/79 + 1;
let q = 79 - e*sin(k) + k/d*(8 + 4*sin(sin(d*d + e/9 - t)));
let c = d/2 - cos(d*2)/5 - t/16 + m;
let px = q*sin(c);
let py = (q + 40)*cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.2 * grow;
let y = 960 + py * 2.2 * grow;
let hue = mod(198 + m*30 + 46*sin(s*0.18 + t*0.35), 360); // cyan ↔ magenta glow
}
// ---- rising bubbles — a second tiny cloud drifting up the tank ----
cloud(bubbles, 130, #dff4ff, 0.5) {
let sp = 0.05 + 0.06 * rand2(i, 1.3); // per-bubble rise speed
let ph = rand2(i, 2.7);
let prog = fract(ph + t * sp); // 0 → 1 rise progress
let x = 1080 * rand2(i, 4.1) + 24 * sin(prog * tau * 2.0 + i);
let y = 1920 * (1.0 - prog); // bottom → top
let r = 2.0 + 5.0 * rand2(i, 5.5);
let hue = 196;
let alpha = 0.5 * sin(prog * pi); // fade in low, fade out near the surface
}
// ---- textbook annotations ----
caption(head, "Jellyfish in a shader aquarium", (540, 138), 33);
caption(sub, "a 20,000-point cloud + a per-pixel ocean", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q{+}40)\cos c)`, 30);
caption(lab, "bells: a polar cloud · water: one shader · bubbles: a second cloud", (540, 1786), 19);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-siphonophore
Another @yuruyurau art-tweet in ONE cloud, reimagined as a bioluminescent deep-sea drifter:
12,000 points trace a morphing bell + long feathered tail (polar radius q, angle c, with a
d^sin(2d-t/3) body-warp) that swims and reconfigures over time. Re-lit in the abyss — additive
glow + bloom make it luminous, the hue drifts cyan bell → violet tail, and faint marine snow
sifts down past it. Pure in (i, t), so it scrubs; the p5 original can’t.
// cloud-siphonophore — another @yuruyurau creature in ONE `cloud`, reimagined as a
// bioluminescent deep-sea drifter. The reference is a tweet-sized golf:
// k = 5cos(i/14)cos(y/30), e = y/8-13, d = (k²+e²)/59 + 6,
// q = 90 - 5sin(atan2(k,e)·e) + k(3+sin(d²-2t)), c = d/2 - t/18,
// point( q·sin c , (q + d·d^sin(2d-t/3))·cos c )
// A morphing bell trailing a long feathered tail (10,000 points, pure in i,t). Here it
// glows in the abyss: additive `glow` + `bloom` make it luminous, the hue drifts along
// the body (cyan bell → violet tail), and faint marine snow sifts down past it.
//
// manic examples/cloud-siphonophore.manic
title("Siphonophore — a yuruyurau creature in the abyss");
canvas("9:16");
template("black");
bloom(0.42, 0.5, 34);
// ---- the abyss: a near-black depth gradient, faint cold light from above ----
shader(abyss) {
let y = v;
let depth = smoothstep(0.0, 1.0, y);
let hue = mix(206, 244, depth);
let val = mix(0.05, 0.006, depth) + 0.02 * (1.0 - smoothstep(0.0, 0.5, y));
let sat = 0.8;
}
z(abyss, -10);
// ---- the creature — the yuruyurau golf, re-lit ----
cloud(crea, 12000, #ffffff, 0.30) {
let yy = i / 43.0;
let k = 5.0 * cos(i / 14.0) * cos(yy / 30.0);
let e = yy / 8.0 - 13.0;
let d = (k*k + e*e) / 59.0 + 6.0;
let T = t * 3.0;
let q = 90.0 - 5.0*sin(atan2(k, e) * e) + k*(3.0 + sin(d*d - T*2.0));
let c = d/2.0 - T/18.0;
let px = q * sin(c);
let py = (q + d * d^sin(d*2.0 - T/3.0)) * cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 3.5 * grow;
let y = 960 + py * 3.5 * grow;
// bioluminescence: cyan bell (small d) → violet tail (large d), shimmering
let hue = mod(184.0 + d * 15.0 + 26.0*sin(yy*0.08 + t*0.4), 360);
let sat = 0.85;
let val = clamp(0.55 + 0.45*sin(d*d - T*2.0), 0.22, 1.0);
let r = 1.35;
}
glow(crea, 1);
// ---- marine snow — a second tiny cloud sifting down past the creature ----
cloud(snow, 150, #dfeeff, 0.4) {
let sp = 0.02 + 0.03 * rand2(i, 1.3);
let ph = rand2(i, 2.7);
let prog = fract(ph + t * sp);
let x = 1080 * rand2(i, 4.1) + 16 * sin(prog * tau + i);
let y = 1920 * prog;
let r = 1.4 + 3.0 * rand2(i, 5.5);
let hue = 200;
let alpha = 0.4 * sin(prog * pi);
}
// ---- annotations ----
caption(head, "Siphonophore in the abyss", (540, 140), 32); hidden(head);
caption(sub, "a 12,000-point cloud, glowing", (540, 206), 22); hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q + d\,d^{\sin(2d-t/3)})\cos c)`, 26); hidden(eq);
caption(lab, "one @yuruyurau golf · additive glow · bloom", (540, 1786), 19); hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(24);
probability-as-area
The two models 3Blue1Brown built the unreleased ‘Essence of Probability’ on, both area arguments rather than formulas. ACT I the BRICK ROW: one brick of probability 1, and every coin flip splits each brick in two - same-outcome bricks merge, so the widths become C(n,k)/2^n and the binomial EMERGES from halving area (1:4:6:4:1 by level four). ACT II the AREA MODEL: a unit square cut across by the prior and down by the test’s accuracy, so Bayes is one bright rectangle over two - a positive test means two chances in three, not ninety percent. No probability builtins were needed: binomial widths are build-time prod/sum reductions and the splits are to(id, width).
// probability-as-area — the two models 3Blue1Brown built the (unreleased) "Essence of
// Probability" series on, both of which are area arguments rather than formulas.
//
// ACT I the BRICK ROW (`_2018/eop/reusables/brick_row.py`): one brick of probability
// 1, and every coin flip splits each brick in two. Bricks with the same number
// of heads merge, so the widths become C(n,k)/2^n and the binomial distribution
// EMERGES from repeatedly halving area — 1 : 4 : 6 : 4 : 1 by level four.
// ACT II the AREA MODEL (`_2018/eop/chapter1/area_model_bayes.py`): a unit square cut
// by the prior across and the test's accuracy down, so Bayes' theorem is just
// one bright rectangle over two.
//
// Written with ordinary manic vocabulary — the binomial coefficients are build-time
// `prod`/`sum` reductions over the loop index, and the splits are `to(id, width, …)` /
// `to(id, height, …)`. Growth is about a rect's centre, so each one is paired with
// `to(id, x, …)` / `to(id, y, …)` in a `par` to anchor the edge that should stay put.
//
// manic examples/probability-as-area.manic
title("Probability is area you keep splitting");
canvas("16:9");
template("black");
bloom(0.3, 0.5, 22);
text(head, (640, 76), "Probability is area you keep splitting");
display(head); size(head, 36); bold(head); color(head, fg); hidden(head);
text(cap, (640, 660), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
// ---- ACT I — the brick row ----
let tw = 880; // the row is one unit of probability, 880px wide
let x0 = 200;
for k in 0..5 {
rect(b{k}, (x0, 330), 0, 104);
hue(b{k}, 188 + 26*k);
}
rect(frame, (x0 + tw/2, 330), tw, 104); outlined(frame); outline(frame, dim); hidden(frame);
text(counts, (640, 262), ""); display(counts); size(counts, 26); color(counts, gold); hidden(counts);
text(flips, (640, 420), ""); display(flips); size(flips, 21); color(flips, dim); hidden(flips);
// ---- ACT II — the area model ----
// a unit square: the prior runs ACROSS (10% of people are ill), the test's accuracy runs
// DOWN (it catches 90% of the ill, and wrongly flags 5% of the well)
let sq = 396;
let sqx = 640; // left edge — the diagram sits right, the maths left
let sqy = 168; // top edge
rect(ill, (sqx, sqy), 0, 0); hue(ill, 320); // ill AND positive
rect(illn, (sqx, sqy), 0, 0); hue(illn, 300); // ill AND negative
rect(wellp, (sqx, sqy), 0, 0); hue(wellp, 196); // well AND positive (false alarm)
rect(well, (sqx + sq/2, sqy + sq/2), sq, sq); hue(well, 208); // everyone, to begin with
hidden(ill); hidden(illn); hidden(wellp); hidden(well);
rect(border, (sqx + sq/2, sqy + sq/2), sq, sq); outlined(border); outline(border, dim); hidden(border);
text(xlab, (sqx + sq/2, sqy + sq + 34), ""); display(xlab); size(xlab, 19); color(xlab, dim); hidden(xlab);
text(ylab, (sqx - 74, sqy + sq/2), ""); display(ylab); size(ylab, 19); color(ylab, dim); hidden(ylab);
equation(bayes, (300, 366), `P(\text{ill}\mid+)=\frac{0.09}{0.09+0.045}=\tfrac{2}{3}`, 27);
color(bayes, gold); hidden(bayes);
// ============================ ACT I ============================
show(head, 0.7);
show(cap, 0.3);
say(cap, "Start with one brick. Its width is the whole of probability: one.");
show(frame, 0.5);
par { to(b0, width, tw, 0.6, smooth); to(b0, x, x0 + tw/2, 0.6, smooth); }
say(counts, "1");
wait(1.2);
say(cap, "A coin flip splits every brick in two — heads to the left, tails to the right.");
show(flips, 0.4);
// each flip: the widths become C(L,k)/2^L. The product is naturally zero for k > L, so a
// brick that does not exist yet simply has no width.
for L in 1..5 {
par {
for k in 0..5 {
to(b{k}, width, tw * prod(j in 1..k+1 : (L - k + j)/j) / 2^L, 0.85, smooth);
to(b{k}, x,
x0 + tw*sum(m in 0..k : prod(j in 1..m+1 : (L - m + j)/j))/2^L
+ tw*prod(j in 1..k+1 : (L - k + j)/j)/(2*2^L),
0.85, smooth);
}
}
wait(0.75);
}
show(counts, 0.4);
say(counts, "1 4 6 4 1");
say(flips, "four flips · widths are C(4,k) / 16");
wait(1.0);
say(cap, "Bricks with the same number of heads merge, and the binomial falls out of the area.");
wait(2.4);
// ============================ ACT II ============================
par {
fade(counts, 0.5);
fade(flips, 0.5);
fade(frame, 0.5);
for k in 0..5 {
fade(b{k}, 0.6);
}
}
say(cap, "The same trick answers a harder question. One square: everybody.");
show(well, 0.5);
show(border, 0.5);
wait(1.4);
say(cap, "Cut it ACROSS by how common the illness is — one person in ten.");
show(xlab, 0.4);
say(xlab, "10% ill · 90% well");
show(ill, 0.01);
par {
// the well column keeps its right edge; the ill column takes the left tenth
to(well, width, 0.9*sq, 0.9, smooth);
to(well, x, sqx + 0.55*sq, 0.9, smooth);
to(ill, width, 0.1*sq, 0.9, smooth);
to(ill, x, sqx + 0.05*sq, 0.9, smooth);
to(ill, height, sq, 0.9, smooth);
to(ill, y, sqy + sq/2, 0.9, smooth);
}
wait(1.6);
say(cap, "Now cut it DOWN by what the test does: it catches 90% of the ill —");
show(ylab, 0.4);
say(ylab, "test + / −");
show(illn, 0.01);
par {
to(ill, height, 0.9*sq, 0.9, smooth);
to(ill, y, sqy + 0.45*sq, 0.9, smooth);
to(illn, width, 0.1*sq, 0.9, smooth);
to(illn, x, sqx + 0.05*sq, 0.9, smooth);
to(illn, height, 0.1*sq, 0.9, smooth);
to(illn, y, sqy + 0.95*sq, 0.9, smooth);
}
wait(1.4);
say(cap, "— and wrongly flags 5% of the well. That thin strip is the false alarms.");
show(wellp, 0.01);
par {
to(well, height, 0.95*sq, 0.9, smooth);
to(well, y, sqy + 0.525*sq, 0.9, smooth);
to(wellp, width, 0.9*sq, 0.9, smooth);
to(wellp, x, sqx + 0.55*sq, 0.9, smooth);
to(wellp, height, 0.05*sq, 0.9, smooth);
to(wellp, y, sqy + 0.025*sq, 0.9, smooth);
}
wait(1.8);
say(cap, "A positive test means you are in one of the two bright rectangles. Which one is bigger?");
par {
fade(illn, 0.6);
fade(well, 0.6);
}
par {
pulse(ill, 0.8);
pulse(wellp, 0.8);
}
wait(1.6);
say(cap, "The false alarms are only half the size — so a positive means two chances in three, not ninety percent.");
show(bayes, 0.8);
wait(3.0);
// ============================ ENDCARD ============================
par {
fade(ill, 0.8);
fade(wellp, 0.8);
fade(border, 0.6);
fade(xlab, 0.5);
fade(ylab, 0.5);
fade(bayes, 0.8);
fade(cap, 0.6);
fade(head, 0.6);
}
text(end1, (640, 348), "Two models, one idea: keep the area, cut it up.");
display(end1); size(end1, 40); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
multivariable-chain-rule
3Blue1Brown’s _2018/mvcr.py, whose single scene is called ComputationalNetwork for a reason:
the multivariable chain rule taught as a GRAPH, and the graph is the one backpropagation runs on.
x feeds f = x^2 and g = cos(pi x); both feed h = f^2 g, so x reaches h along TWO paths. Build the
network box by box, feed 2 forward (f=4, g=1, h=16), then nudge x and watch h swing 32 times as
far - the h dot is a one-point cloud reading the scene parameter, the only way to place a dot
where a NONLINEAR function of a live value says it goes. Then one derivative per EDGE, one product
per PATH, summed: mathparts makes each term its own entity so a framebox can surround them in
turn, and rewrite carries the equation from symbolic to substituted to 32 + 0 = 32. It ends by
running the same rule backwards through a real network - which is what backpropagation is.
// multivariable-chain-rule — 3Blue1Brown's `_2018/mvcr.py`, whose single scene is called
// `ComputationalNetwork` for a reason: the multivariable chain rule is taught as a GRAPH,
// and the graph is the one backpropagation runs on.
//
// x ──→ f = x² ──→ h = f²g
// └──→ g = cos(πx) ──┘
//
// ACT I build the network, box by box, edge by edge
// ACT II feed one number forward: x=2 → f=4, g=1 → h=16
// ACT III the question — nudge x, and h swings THIRTY-TWO times as far
// ACT IV one derivative per EDGE, and one term per PATH from x to h
// ACT V assemble, substitute, evaluate: 32 + 0 = 32
// ACT VI the same rule with a thousand nodes has another name
//
// Written with ordinary vocabulary: `mathparts` makes each term of the rule its own entity
// so a `framebox` can `surround` them in turn, `rewrite` carries one equation through its
// symbolic → substituted → evaluated states, and the sensitivity beat is a one-point
// `cloud` reading the scene `parameter` — the only way to put a dot where a NONLINEAR
// function of a live value says it goes.
//
// manic examples/multivariable-chain-rule.manic
title("The Multivariable Chain Rule — One Term Per Path");
canvas("16:9");
template("black");
bloom(0.28, 0.55, 20);
// ---- type ----
text(ttl, (640, 92), "The Multivariable Chain Rule");
display(ttl); size(ttl, 46); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 146), "one term per path through the graph");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 664), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1060, 620), ""); display(act); size(act, 19); color(act, gold); hidden(act);
// ================================ THE NETWORK ================================
// four boxed formulas. The colour of a letter is the colour of its node, everywhere it
// appears — that is the whole reading aid, and LaTeX does it inline.
equation(ex, (196, 300), `\textcolor{gold}{x}`, 34);
equation(ef, (520, 206), `\textcolor{cyan}{f} = \textcolor{gold}{x}^2`, 30);
equation(eg, (520, 396), `\textcolor{lime}{g} = \cos(\pi \textcolor{gold}{x})`, 30);
equation(eh, (912, 300), `\textcolor{magenta}{h} = \textcolor{cyan}{f}^2 \textcolor{lime}{g}`, 30);
hidden(ex); hidden(ef); hidden(eg); hidden(eh);
framebox(bx, ex, 14); color(bx, dim); untraced(bx);
framebox(bf, ef, 14); color(bf, dim); untraced(bf);
framebox(bg, eg, 14); color(bg, dim); untraced(bg);
framebox(bh, eh, 14); color(bh, dim); untraced(bh);
// the four edges, each an arrow between two boxes
arrow(axf, (250, 282), (410, 218)); color(axf, dim); untraced(axf);
arrow(axg, (250, 318), (410, 384)); color(axg, dim); untraced(axg);
arrow(afh, (636, 218), (800, 282)); color(afh, dim); untraced(afh);
arrow(agh, (636, 384), (800, 318)); color(agh, dim); untraced(agh);
tag(axf, edges); tag(axg, edges); tag(afh, edges); tag(agh, edges);
// ---- the forward pass ----
equation(vx, (196, 352), `= \textcolor{gold}{2}`, 26); color(vx, gold); hidden(vx);
equation(vf, (520, 256), `= \textcolor{cyan}{4}`, 26); color(vf, cyan); hidden(vf);
equation(vg, (520, 446), `= \textcolor{lime}{1}`, 26); color(vg, lime); hidden(vg);
equation(vh, (912, 352), `= \textcolor{magenta}{16}`, 26); color(vh, magenta); hidden(vh);
// ---- one derivative per edge ----
equation(dfx, (300, 196), `\frac{df}{dx} = 2x`, 24); color(dfx, cyan); hidden(dfx);
equation(dgx, (300, 424), `\frac{dg}{dx} = -\pi\sin(\pi x)`, 24); color(dgx, lime); hidden(dgx);
equation(dhf, (742, 196), `\frac{\partial h}{\partial f} = 2fg`, 24); color(dhf, cyan); hidden(dhf);
equation(dhg, (742, 424), `\frac{\partial h}{\partial g} = f^2`, 24); color(dhg, lime); hidden(dhg);
// ================================ THE SENSITIVITY ================================
// x lives on 0..4, h on 0..32 — the same nudge is small on one line and large on the other
parameter(xv, (150, 606), 2, 1.8, 2.2, "x", 3); hidden(xv.widget);
numberline(xline, (400, 300), 200, 0, 4, 1); color(xline, dim); hidden(xline);
numberline(hline, (900, 440), 260, 0, 32, 8); color(hline, dim); hidden(hline);
text(xtag, (400, 236), "x"); display(xtag); size(xtag, 24); color(xtag, gold); hidden(xtag);
equation(htag, (900, 372), `h = x^4\cos(\pi x)`, 26); color(htag, magenta); hidden(htag);
dot(xdot, (400, 300), 8); color(xdot, gold); hidden(xdot);
bind(xv, xdot, x, 380, 420); // 1.8 and 2.2 in the line's OWN pixels: a small nudge
// the h dot has to sit where a nonlinear function of the live parameter says: one point,
// one formula, re-evaluated every frame
cloud(hdot, 1, magenta) {
let hv = xv*xv*xv*xv*cos(pi*xv);
let x = 640 + 520*hv/32; // the line's 0 is at 640, its 32 at 1160
let y = 440;
let r = 8;
}
hidden(hdot);
// ================================ THE RULE ================================
// each part is its own entity, so a highlight box can visit them one at a time
mathparts(rule, (640, 520),
`\frac{dh}{dx} =`,
`\;\frac{df}{dx}\frac{\partial h}{\partial f}`,
`\; + \;`,
`\frac{dg}{dx}\frac{\partial h}{\partial g}`,
30);
color(rule.0, fg); color(rule.1, cyan); color(rule.2, dim); color(rule.3, lime);
hidden(rule);
framebox(mark, rule.1, 10); color(mark, gold); hidden(mark);
equation(subst, (640, 588), `= (2\cdot 2)(2\cdot 4\cdot 1) \; + \; (-\pi\sin 2\pi)(4^2)`, 28);
color(subst, fg); hidden(subst);
// ================================ THE CODA ================================
network(net, (640, 380), "3 5 4 2", "relu relu softmax", 620, 380, 7);
hidden(net);
// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.2);
show(cap, 0.3);
say(cap, "One input. Two things computed from it. One thing computed from those two.");
par { fade(ttl, 0.8); fade(sub, 0.8); }
show(ex, 0.5);
draw(bx, 0.5);
wait(0.5);
par { draw(axf, 0.6); draw(axg, 0.6); }
par { show(ef, 0.6); show(eg, 0.6); }
par { draw(bf, 0.5); draw(bg, 0.5); }
wait(0.6);
say(cap, "Both of them feed the same last box, so x reaches h along TWO different paths.");
par { draw(afh, 0.6); draw(agh, 0.6); }
show(eh, 0.6);
draw(bh, 0.5);
wait(2.0);
// ================================= ACT II =================================
say(act, "II · feed it forward");
show(act, 0.4);
say(cap, "Put in x = 2. Everything downstream follows: f is 4, g is 1, so h is 16.");
stagger(0.5) {
show(vx, 0.4);
show(vf, 0.4);
show(vg, 0.4);
show(vh, 0.4);
}
wait(1.8);
// ================================= ACT III =================================
say(act, "III · how sensitive is h?");
say(cap, "Now the only question that matters: nudge x a little — how far does h move?");
par {
to(ex, opacity, 0.25, 0.6);
to(ef, opacity, 0.25, 0.6);
to(eg, opacity, 0.25, 0.6);
to(eh, opacity, 0.25, 0.6);
to(vx, opacity, 0.2, 0.6);
to(vf, opacity, 0.2, 0.6);
to(vg, opacity, 0.2, 0.6);
to(vh, opacity, 0.2, 0.6);
to(edges, opacity, 0.2, 0.6);
to(bx, opacity, 0.15, 0.6);
to(bf, opacity, 0.15, 0.6);
to(bg, opacity, 0.15, 0.6);
to(bh, opacity, 0.15, 0.6);
}
par { show(xline, 0.5); show(hline, 0.5); }
par { show(xtag, 0.4); show(htag, 0.5); show(xdot, 0.4); show(hdot, 0.4); }
wait(0.8);
say(cap, "Watch the two dots. The same wiggle, on two very different scales.");
to(xv, value, 2.2, 1.1, smooth);
to(xv, value, 1.8, 1.6, smooth);
to(xv, value, 2, 0.9, smooth);
wait(0.6);
say(cap, "x moved a fifth of a unit. h moved six. The ratio it settles on is dh/dx = 32.");
wait(2.4);
// ================================= ACT IV =================================
say(act, "IV · one derivative per edge");
par {
fade(xline, 0.5); fade(hline, 0.5); fade(xtag, 0.4); fade(htag, 0.5);
fade(xdot, 0.4); fade(hdot, 0.4);
to(ex, opacity, 1, 0.6);
to(ef, opacity, 1, 0.6);
to(eg, opacity, 1, 0.6);
to(eh, opacity, 1, 0.6);
to(edges, opacity, 1, 0.6);
to(bx, opacity, 1, 0.6);
to(bf, opacity, 1, 0.6);
to(bg, opacity, 1, 0.6);
to(bh, opacity, 1, 0.6);
to(vx, opacity, 0.35, 0.6);
to(vf, opacity, 0.35, 0.6);
to(vg, opacity, 0.35, 0.6);
to(vh, opacity, 0.35, 0.6);
}
say(cap, "Every EDGE carries a derivative: how much its head moves when its tail moves.");
par { show(dfx, 0.5); show(dgx, 0.5); }
wait(1.4);
say(cap, "The last two are PARTIAL — hold the other input still while you wiggle this one.");
par { show(dhf, 0.5); show(dhg, 0.5); }
wait(2.2);
say(cap, "Follow the top path: x changes f, f changes h. Multiply the two.");
par { pulse(axf, 0.8); pulse(afh, 0.8); }
wait(1.6);
say(cap, "Then the bottom path: x changes g, g changes h. Multiply those too — and ADD.");
par { pulse(axg, 0.8); pulse(agh, 0.8); }
wait(2.0);
// ================================= ACT V =================================
say(act, "V · assemble it");
say(cap, "That is the whole rule: one product per path, summed over every path.");
show(rule, 0.8);
wait(1.2);
show(mark, 0.5);
say(cap, "The top path — df/dx times the partial of h in f.");
wait(1.6);
surround(mark, rule.3, 0.8, smooth);
say(cap, "The bottom path — dg/dx times the partial of h in g. Nothing else contributes.");
wait(2.0);
fade(mark, 0.5);
say(cap, "Substitute what we know at x = 2, where f = 4 and g = 1.");
show(subst, 0.8);
wait(2.2);
say(cap, "Sine of two pi is zero, so the bottom path contributes NOTHING here. 32 plus 0.");
rewrite(subst, `= 32 \; + \; 0 \;=\; \textcolor{magenta}{32}`, 1.4, smooth);
wait(2.6);
say(cap, "The same 32 the wiggling dots found — and h = x⁴cos(πx) agrees, if you expand it.");
wait(2.4);
// ================================= ACT VI =================================
say(act, "VI · at scale");
par {
fade(ex, 0.6); fade(ef, 0.6); fade(eg, 0.6); fade(eh, 0.6);
fade(bx, 0.5); fade(bf, 0.5); fade(bg, 0.5); fade(bh, 0.5);
fade(edges, 0.5);
fade(vx, 0.4); fade(vf, 0.4); fade(vg, 0.4); fade(vh, 0.4);
fade(dfx, 0.5); fade(dgx, 0.5); fade(dhf, 0.5); fade(dhg, 0.5);
fade(rule, 0.7); fade(subst, 0.7);
}
say(cap, "Four boxes and two paths. Now give the same rule a few thousand of each.");
show(net, 1.0);
forward(net, "0.9 0.2 0.6", 2.0);
wait(0.6);
loss(net, "1 0", crossentropy, 1.2);
say(cap, "Every weight is an edge, every edge carries a derivative, every path gets summed.");
backward(net, 3.0, smooth);
wait(1.6);
say(cap, "Run it backwards and the chain rule has another name: backpropagation.");
wait(2.6);
// ================================= ENDCARD =================================
par {
fade(net, 0.9);
fade(cap, 0.7);
fade(act, 0.6);
}
text(end1, (640, 340), "One product per path. Sum over paths.");
display(end1); size(end1, 42); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
sphere-area
Why a sphere’s area is 4piR^2 — 3Blue1Brown’s _2018/sphere_area.py, which is to say why a
sphere is exactly FOUR of its own shadows. ACT I the question: a sphere, the disc it shadows,
and the factor of four. ACT II Archimedes’ map — the sphere is cut into 336 tiles (pieces3)
and every tile is pushed radially outward onto the enclosing cylinder; a cross-section shows
why nothing tears (a tile at distance d lands at R, so its width scales by R/d while it leans
by d/R, and the product is 1). ACT III that cylinder unrolls into a flat 2piR x 2R rectangle.
ACT IV four discs, each unrolled ring by ring into a right triangle of base 2piR and height R,
tile that rectangle exactly. Sphere, cylinder and flat sheet are ONE param3 on one parameter
journey, and the tiles re-sample it every frame, so the whole tiling rides the map.
// sphere-area — 3Blue1Brown's `_2018/sphere_area.py`: why the area of a sphere is 4πR²,
// which is to say why it is exactly FOUR of its own shadows.
//
// ACT I the question — a sphere, its shadow, and the factor of four
// ACT II Archimedes' map — cut the sphere into tiles (`pieces3`) and push every tile
// radially outward onto the enclosing cylinder. Nothing tears: each tile gets
// WIDER by R/d and SHORTER by d/R, and R/d · d/R = 1, so area is preserved.
// ACT III unwrap that cylinder — a flat 2πR × 2R rectangle
// ACT IV and four discs, each unrolled ring by ring into a right triangle of area πR²,
// tile it exactly. Four circles. That is the whole answer.
//
// The sphere, the cylinder and the flat rectangle are ONE `param3` on one parameter journey
// (0 → sphere, 1 → cylinder, 2 → unwrapped), and the tiles are `pieces3` of it — so the
// pieces are re-sampled from the surface's own formulas every frame and travel with it.
// The four unrolling discs are `cloud`s reading the same kind of parameter. R = 1 throughout,
// so the numbers on screen are the theorem: 2π · 2 = 4π.
//
// manic examples/sphere-area.manic
title("Sphere Area — Four Circles, Wrapped");
canvas("16:9");
template("black");
bloom(0.3, 0.5, 22);
// ---- type ----
text(ttl, (640, 96), "Why 4πR²?");
display(ttl); size(ttl, 52); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 152), "a sphere is exactly four of its own shadows");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 664), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1050, 622), ""); display(act); size(act, 19); color(act, gold); hidden(act);
// ================================ THE 3-D STAGE ================================
camera3((4.6, -5.4, 3.2), (0, 0, 0), 46, perspective);
// One surface, three shapes. `map` = 0 the unit sphere, 1 the enclosing cylinder (every
// point pushed straight out from the axis, keeping its height), 2 that cylinder unwrapped
// into the flat 2π × 2 rectangle. `clamp` splits the journey into its two halves.
parameter(map, (150, 606), 0, 0, 2, "map", 2); hidden(map.widget);
param3(shell,
"(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*cos(u) + clamp(p-1,0,1)*(u-pi)",
"(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*sin(u) - clamp(p-1,0,1)*cos(v)",
"(1-clamp(p-1,0,1))*cos(v)",
(0, tau), (0.02, 3.12), 28);
bind(map, shell, formula,
"(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*cos(u) + clamp(p-1,0,1)*(u-pi)",
"(1-clamp(p-1,0,1))*((1-clamp(p,0,1))*sin(v) + clamp(p,0,1))*sin(u) - clamp(p-1,0,1)*cos(v)",
"(1-clamp(p-1,0,1))*cos(v)");
color(shell, cyan); finish3(shell, "wire=1"); hidden(shell);
// the same surface as 336 loose tiles — they re-sample the surface every frame, so the
// whole tiling rides the map out onto the cylinder and then flat
pieces3(tiles, shell, 24, 14, 0.12);
hue(tiles, 196);
hue(tiles.row7, 320); // one latitude band, to watch a single row travel
hidden(tiles);
// the shadow: the disc the sphere covers, on the ground
param3(shade, "v*cos(u)", "v*sin(u)", "-1.05", (0, tau), (0.02, 1), 24);
color(shade, gold); hidden(shade);
grid3(floor, (0, 0, -1.06), 2, 0.5); color(floor, dim); hidden(floor);
// ================================ THE 2-D STAGE ================================
// the unwrapped rectangle, in screen space. R = 110px, so it is 2piR = 691 wide and 2R = 220
// tall, and four discs of radius R fit in a row above it — the areas on screen are the ones
// in the argument, not a convenient cartoon.
rect(sheet, (640, 430), 691, 220); outlined(sheet); outline(sheet, dim); hidden(sheet);
text(wlab, (640, 566), "2πR"); display(wlab); size(wlab, 22); color(wlab, cyan); hidden(wlab);
text(hlab, (250, 430), "2R"); display(hlab); size(hlab, 22); color(hlab, magenta); hidden(hlab);
equation(area, (640, 214), `2\pi R \cdot 2R = 4\pi R^2`, 34); color(area, gold); hidden(area);
// Four discs, each unrolled ring by ring into a right triangle that lands in the sheet: a
// ring of radius r straightens into a segment 2*pi*r long, so the stack of them IS a
// triangle of base 2piR, height R, area piR^2. Two of them tile each half of the sheet.
// 12,000 points as 300 angles x 40 radii, so the long outer rings stay solid when straight.
parameter(un, (150, 606), 0, 0, 1, "unrolled", 2); hidden(un.widget);
cloud(d0, 12000, cyan) {
let r = (mod(i, 40) + 0.5)/40;
let th = floor(i/40) * 0.020944;
let x = (1-un)*(200 + 110*r*cos(th)) + un*(294 + 110*r*th);
let y = (1-un)*(170 + 110*r*sin(th)) + un*(320 + 110*r);
let rr = 1.5;
let hue = 196;
}
cloud(d1, 12000, cyan) {
let r = (mod(i, 40) + 0.5)/40;
let th = floor(i/40) * 0.020944;
let x = (1-un)*(420 + 110*r*cos(th)) + un*(985 - 110*r*th);
let y = (1-un)*(170 + 110*r*sin(th)) + un*(430 - 110*r);
let rr = 1.5;
let hue = 220;
}
cloud(d2, 12000, cyan) {
let r = (mod(i, 40) + 0.5)/40;
let th = floor(i/40) * 0.020944;
let x = (1-un)*(640 + 110*r*cos(th)) + un*(294 + 110*r*th);
let y = (1-un)*(170 + 110*r*sin(th)) + un*(430 + 110*r);
let rr = 1.5;
let hue = 288;
}
cloud(d3, 12000, cyan) {
let r = (mod(i, 40) + 0.5)/40;
let th = floor(i/40) * 0.020944;
let x = (1-un)*(860 + 110*r*cos(th)) + un*(985 - 110*r*th);
let y = (1-un)*(170 + 110*r*sin(th)) + un*(540 - 110*r);
let rr = 1.5;
let hue = 324;
}
hidden(d0); hidden(d1); hidden(d2); hidden(d3);
// ---- the lemma, as a cross-section ----
circle(cs, (420, 380), 150); outlined(cs); outline(cs, dim); hidden(cs);
line(axis, (420, 200), (420, 560)); color(axis, dim); hidden(axis);
line(wall, (570, 200), (570, 560)); color(wall, cyan); hidden(wall);
line(ray, (420, 380), (570, 275)); color(ray, gold); untraced(ray);
line(dseg, (420, 294), (543, 294)); color(dseg, magenta); untraced(dseg);
dot(tile, (543, 294), 5); color(tile, cyan); hidden(tile);
text(dlab, (478, 270), "d"); display(dlab); size(dlab, 20); color(dlab, magenta); hidden(dlab);
text(rlab, (492, 352), "R"); display(rlab); size(rlab, 20); color(rlab, gold); hidden(rlab);
equation(wide, (860, 320), `\text{width} \times \tfrac{R}{d}`, 30); color(wide, cyan); hidden(wide);
equation(short, (860, 396), `\text{height} \times \tfrac{d}{R}`, 30); color(short, magenta); hidden(short);
equation(one, (860, 480), `\tfrac{R}{d}\cdot\tfrac{d}{R}=1`, 30); color(one, gold); hidden(one);
// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.4);
show(cap, 0.3);
say(cap, "A sphere of radius R. Roll it in your hand: how much surface is there?");
show(shell, 0.9);
show(floor, 0.5);
orbit3(34, 22, 4.6, 2.6, smooth);
wait(0.8);
par { fade(ttl, 0.8); fade(sub, 0.8); }
say(cap, "Here is its shadow — a circle of area πR². The sphere's area is exactly four of those.");
show(shade, 0.8);
pulse(shade, 0.9);
wait(2.2);
say(cap, "Four. Not π, not 2π. Four circles' worth of paper, wrapped on a ball. Why?");
wait(2.4);
// ================================= ACT II =================================
say(act, "II · onto a cylinder");
show(act, 0.4);
par { fade(shade, 0.6); fade(floor, 0.5); }
say(cap, "Cut the surface into tiles. Nothing about the sphere has changed yet.");
par { fade(shell, 0.7); show(tiles, 0.9); }
wait(1.6);
say(cap, "Now push every tile straight out from the axis, onto the cylinder that encloses it.");
show(map.widget, 0.5);
to(map, value, 1, 3.2, smooth);
wait(1.0);
say(cap, "Watch one band. It moved out, so it got wider — and it tilted flat, so it got shorter.");
pulse(tiles.row7, 0.9);
wait(2.2);
say(cap, "That trade is exact. Cut the ball in half and it is two similar triangles.");
par {
fade(tiles, 0.8);
fade(map.widget, 0.5);
}
show(cs, 0.6);
show(axis, 0.5);
show(wall, 0.6);
wait(0.6);
draw(ray, 0.7);
draw(dseg, 0.5);
show(tile, 0.4);
show(dlab, 0.4);
show(rlab, 0.4);
wait(1.4);
say(cap, "A tile at distance d from the axis lands at distance R, so its width scales by R/d.");
show(wide, 0.7);
wait(2.0);
say(cap, "And the surface there leans by the same ratio, so its height squishes by d/R.");
show(short, 0.7);
wait(2.0);
say(cap, "One stretch, one squish, the same number. The tile's AREA never changed.");
show(one, 0.8);
wait(2.4);
// ================================= ACT III =================================
say(act, "III · unwrap it");
par {
fade(cs, 0.6); fade(axis, 0.5); fade(wall, 0.5); fade(ray, 0.5); fade(dseg, 0.5);
fade(dlab, 0.4); fade(rlab, 0.4); fade(tile, 0.4); fade(wide, 0.6); fade(short, 0.6); fade(one, 0.6);
}
say(cap, "So the sphere and the cylinder have the same area — and a cylinder unrolls flat.");
show(tiles, 0.8);
orbit3(-96, 54, 6.4, 2.4, smooth);
to(map, value, 2, 3.0, smooth);
wait(1.2);
say(cap, "A rectangle. Its height is 2R, and its width is the cylinder's circumference, 2πR.");
wait(2.4);
par { fade(tiles, 0.9); }
show(sheet, 0.7);
show(wlab, 0.5);
show(hlab, 0.5);
wait(1.0);
show(area, 0.9);
say(cap, "Two π R, times two R. Four π R squared — the sphere's area, with nothing left over.");
wait(2.8);
// ================================= ACT IV =================================
say(act, "IV · and the four circles");
fade(area, 0.7);
say(cap, "One thing is still owed: why FOUR circles fill that rectangle. Here are four.");
par {
show(d0, 0.6); show(d1, 0.6); show(d2, 0.6); show(d3, 0.6);
}
wait(1.6);
say(cap, "Unroll each one ring by ring. A ring of radius r straightens into a line 2πr long.");
show(un.widget, 0.5);
to(un, value, 1, 3.4, smooth);
wait(1.0);
say(cap, "Each circle becomes a right triangle: base 2πR, height R, area πR². Four of them —");
wait(2.4);
say(cap, "— and they tile the rectangle exactly. A sphere is four of its own shadows.");
show(area, 0.9);
wait(3.0);
// ================================= ENDCARD =================================
par {
fade(d0, 0.8); fade(d1, 0.8); fade(d2, 0.8); fade(d3, 0.8);
fade(sheet, 0.6); fade(wlab, 0.5); fade(hlab, 0.5); fade(area, 0.8);
fade(un.widget, 0.5); fade(cap, 0.7); fade(act, 0.6);
}
text(end1, (640, 340), "Push it out, unroll it, count the circles.");
display(end1); size(end1, 42); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 420), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
quaternions
Quaternions, as 3Blue1Brown builds them in _2018/quaternions.py — not a formula with four
letters in it, but four pictures, each the last one with a dimension added. ACT I warp shows
multiplying by 1+i turning AND stretching the plane, then a unit number turning it rigidly. ACT II
a circle becomes a line: the pole, three construction rays, and 520 beads sliding out to 2·tan(a/2).
ACT III the same thing one dimension up — a wire globe (param3 + bind) peels off its pole and
lies down as an honest disc. ACT IV the unit quaternions ARE a 3-sphere, so it has no picture: 6,000
points on twelve linked HOPF FIBRES, stereographically projected, left-multiplied by cos θ + j sin θ
from one dial. Every act is driven by a parameter, not by the clock — including the 3-sphere, whose
cloud is a closed-form function of (i, dial): a Hamilton product, then a projection.
// quaternions — the argument 3Blue1Brown builds in `_2018/quaternions.py`, which is not
// "here is a formula with four letters in it" but a chain of four pictures, each one the
// last one with a dimension added:
//
// ACT I multiplying complex numbers TURNS the plane (`warp`, z → q·z)
// ACT II a circle is a line, seen from the pole (stereographic, 2-D)
// ACT III a sphere is a plane, seen from the pole (`param3` + `bind`)
// ACT IV the unit quaternions are a 3-SPHERE — project it into 3-space and left
// multiplication becomes a visible flow (`cloud3` + `parameter`)
//
// The point of the sequence is that you never see four dimensions; you see a shadow of
// them, and multiplication is a rigid turn of the thing casting it.
//
// Everything here is driven by scene `parameter`s rather than by time, so each picture is
// a DIAL you can stop anywhere — including the 3-sphere, whose 5,000 particles are a
// closed-form function of (i, dial): a Hamilton product, then a projection.
//
// manic examples/quaternions.manic
title("Quaternions — Turning in Four Dimensions");
canvas("16:9");
template("black");
bloom(0.32, 0.55, 24);
// ---- type ----
text(ttl, (640, 92), "Quaternions");
display(ttl); size(ttl, 54); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 148), "turning in four dimensions");
display(sub); size(sub, 23); color(sub, dim); hidden(sub);
text(cap, (640, 662), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1122, 620), ""); display(act); size(act, 19); color(act, gold); hidden(act);
// ============================ ACT I — a turn of the plane ============================
// z → (1+i)z turns AND stretches; z → (cos60 + i sin60)z only turns, because |q| = 1.
warp(gstretch, (770, 366), 64, `(1+i)*z`, 3, 34); color(gstretch, dim); hidden(gstretch);
warp(gturn, (770, 366), 64, `(0.5+0.8660254*i)*z`, 3, 34); color(gturn, cyan); hidden(gturn);
equation(eq1, (228, 340), `z \mapsto (1+i)\,z`, 30); color(eq1, gold); hidden(eq1);
equation(eq2, (228, 340), `z \mapsto (\cos 60^{\circ}+i\sin 60^{\circ})\,z`, 26);
color(eq2, gold); hidden(eq2);
// ============================ ACT II — the circle and the line ======================
parameter(flat, (168, 596), 0, 0, 1, "projected", 2); hidden(flat.widget);
circle(hoop, (640, 300), 140); outlined(hoop); outline(hoop, dim); hidden(hoop);
line(axis, (140, 440), (1140, 440)); color(axis, dim); hidden(axis);
dot(pole, (640, 160), 6); color(pole, gold); hidden(pole);
text(plab, (640, 132), "the pole you look from");
display(plab); size(plab, 17); color(plab, gold); hidden(plab);
// three rays of the construction: from the pole, through a point of the circle, to the line
line(ray1, (640, 160), (831, 440)); color(ray1, magenta); untraced(ray1);
line(ray2, (640, 160), (448, 440)); color(ray2, magenta); untraced(ray2);
line(ray3, (640, 160), (1032, 440)); color(ray3, magenta); untraced(ray3);
// the circle's own points, which slide out along those rays as `flat` opens
// x_line = 2·tan(a/2): a point near the pole lands far away, and the pole itself never lands
cloud(beads, 520, cyan) {
let a = -2.0 + (i/520)*4.0;
let px = sin(a);
let py = -cos(a);
let xl = 2*sin(a)/(1 + cos(a));
let x = 640 + 140*(px*(1-flat) + xl*flat);
let y = 300 - 140*(py*(1-flat) - flat);
let r = 2.4;
let hue = 188 + 18*flat;
}
hidden(beads);
// ============================ ACT III & IV — the 3-D stage =========================
camera3((4.8, -5.4, 3.6), (0, 0, 0), 44, perspective);
// The sphere, morphing into its own stereographic projection — the same picture as ACT II
// with one more dimension, and the same dial. It is parametrized AROUND the pole you look
// from (`v` is the angle away from it, stopping just short at 2.45 rad), so the flattened
// picture is an honest disc: radius tan(v/2), the pole itself infinitely far out.
parameter(proj, (168, 596), 0, 0, 1, "projected", 2); hidden(proj.widget);
param3(ball,
"(1-p)*sin(v)*cos(u) + 0.4*p*sin(v)*cos(u)/(1+cos(v))",
"(1-p)*sin(v)*sin(u) + 0.4*p*sin(v)*sin(u)/(1+cos(v))",
"(1-p)*cos(v)",
(0, tau), (0.16, 2.45), 26);
bind(proj, ball, formula,
"(1-p)*sin(v)*cos(u) + 0.4*p*sin(v)*cos(u)/(1+cos(v))",
"(1-p)*sin(v)*sin(u) + 0.4*p*sin(v)*sin(u)/(1+cos(v))",
"(1-p)*cos(v)");
color(ball, cyan); finish3(ball, "wire=1"); hidden(ball); // a wire globe reads as a GRID, and the grid is what gets carried to the plane
// THE 3-SPHERE. Every unit quaternion q = w + xi + yj + zk with |q| = 1 lives on it: a
// 3-dimensional surface in 4-space, so it has no picture — but its shadow in 3-space does.
// The twelve circles below are HOPF FIBRES over a ring of directions: great circles of the
// 3-sphere, every pair of them linked, which stereographic projection carries to linked
// circles here. Each is sampled by ARC LENGTH (the `atan2` reparametrization), or the
// projection would bunch every dot at the near side. `dial` left-multiplies all 6,000 of
// them by cos θ + j sin θ — a rigid turn of the 3-sphere, which the shadow has to bend to
// follow. The pole is set just outside (`d = rw + 1.12`) so no circle ever runs off to
// infinity mid-turn.
parameter(dial, (150, 596), 0, 0, 1, "θ", 2); hidden(dial.widget);
cloud3(s3, 6000, #00e5ff, 0.6) {
let f = mod(i, 12); // which fibre
let a0 = 0.95; // the ring of directions they sit over
let ph = f * 0.5235988; // where this one sits around that ring
let u = floor(i/12) * 0.0125664; // 500 samples along the fibre
let c = cos(a0);
let ec = sqrt((1+c)/(1-c));
let sp = 2*atan2(ec*sin(u/2), cos(u/2)); // uniform spacing AFTER projection
let qw = c*cos(sp); // the fibre itself: a great circle of S³
let qx = c*sin(sp);
let qy = sin(a0)*cos(sp + ph);
let qz = sin(a0)*sin(sp + ph);
let ang = dial * tau;
let m0 = cos(ang);
let m2 = sin(ang);
let rw = m0*qw - m2*qy; // the Hamilton product (cos θ + j sin θ)·q
let rx = m0*qx + m2*qz;
let ry = m0*qy + m2*qw;
let rz = m0*qz - m2*qx;
let d = rw + 1.12;
let x = rx / d;
let y = ry / d;
let z = rz / d;
let r = 0.013;
let hue = 186 + 100*f/12;
let alpha = 0.6;
}
glow(s3, 2); hidden(s3);
equation(ham, (250, 210), `i^2=j^2=k^2=ijk=-1`, 27); color(ham, gold); hidden(ham);
text(hlab, (250, 262), "Hamilton, on a bridge in Dublin, 1843");
display(hlab); size(hlab, 17); color(hlab, dim); hidden(hlab);
// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.5);
show(cap, 0.3);
say(cap, "Start in the complex plane, where multiplying does something to ALL of it.");
par { fade(ttl, 0.8); fade(sub, 0.8); }
show(gstretch, 0.7);
show(eq1, 0.6);
wait(0.6);
to(gstretch, morph, 1, 2.0, smooth);
wait(0.9);
say(cap, "Multiply by 1+i and the plane turns — and stretches, since 1+i is longer than 1.");
wait(2.0);
par { fade(gstretch, 0.6); fade(eq1, 0.5); }
say(cap, "Pick a number of length exactly one, and the stretching stops.");
show(gturn, 0.6);
show(eq2, 0.6);
to(gturn, morph, 1, 2.2, smooth);
wait(1.4);
say(cap, "A unit complex number IS a rotation. That is the whole idea — the rest is dimensions.");
wait(2.6);
// ================================= ACT II =================================
par { fade(gturn, 0.8); fade(eq2, 0.6); }
say(act, "II · a circle is a line");
show(act, 0.4);
say(cap, "Before four dimensions, do two. Here is a circle, and a line it just touches.");
show(hoop, 0.7);
show(axis, 0.6);
show(beads, 0.7);
wait(1.4);
say(cap, "Stand at the top. Look through any point of the circle, and you land on the line.");
show(pole, 0.5);
show(plab, 0.4);
par { draw(ray1, 0.6); draw(ray2, 0.6); draw(ray3, 0.7); }
wait(1.8);
say(cap, "Every point of the circle has its own place on the line — so let them go there.");
show(flat.widget, 0.5);
to(flat, value, 1, 2.6, smooth);
wait(1.0);
say(cap, "The circle became the line. Only the pole is missing — it would land infinitely far.");
wait(2.4);
say(cap, "One missing point, in exchange for a flat picture. It works in any dimension.");
wait(2.6);
// ================================= ACT III =================================
par {
fade(beads, 0.8); fade(hoop, 0.6); fade(axis, 0.6);
fade(ray1, 0.5); fade(ray2, 0.5); fade(ray3, 0.5);
fade(pole, 0.5); fade(plab, 0.5); fade(flat.widget, 0.5);
}
say(act, "III · a sphere is a plane");
say(cap, "One dimension up: a sphere, and the same pole to look from.");
show(ball, 0.9);
orbit3(28, 22, 4.8, 2.6, smooth);
wait(1.0);
say(cap, "Open the same dial. The sphere peels off the pole and lies down flat.");
show(proj.widget, 0.5);
to(proj, value, 1, 3.0, smooth);
wait(1.2);
say(cap, "A sphere is a plane plus one point. Nothing tore; the pole was sent away.");
orbit3(64, 62, 4.9, 3.0, smooth);
wait(2.2);
// ================================= ACT IV =================================
par { fade(ball, 0.9); fade(proj.widget, 0.5); }
say(act, "IV · the unit quaternions");
say(cap, "Now four. Take every quaternion of length one: w² + x² + y² + z² = 1.");
show(ham, 0.7);
show(hlab, 0.5);
wait(1.8);
say(cap, "A three-dimensional surface in four-dimensional space: a 3-sphere. It has no picture.");
wait(2.2);
say(cap, "But it has a shadow. Project from a pole, as before, and it fits in this room.");
par {
fade(ham, 0.8);
fade(hlab, 0.6);
}
show(s3, 1.2);
orbit3(-24, 22, 4.6, 3.2, smooth);
wait(1.2);
say(cap, "Six thousand of them, on twelve great circles — and every pair is linked.");
wait(2.2);
say(cap, "Multiply every one by cos θ + j sin θ — Act I's move, one dimension up.");
show(dial.widget, 0.6);
to(dial, value, 0.5, 4.0, smooth);
wait(0.4);
say(cap, "Nothing is being deformed. The 3-sphere is turning rigidly; only its shadow bends.");
to(dial, value, 1, 4.0, smooth);
wait(0.6);
say(cap, "Half a turn of the dial sent 1 to −1. A full turn brings every point home.");
orbit3(78, -18, 5.0, 4.0, smooth);
wait(2.0);
say(cap, "A quaternion multiplication: a rotation you can only watch in shadow.");
wait(2.6);
// ================================= ENDCARD =================================
par {
fade(s3, 1.2);
fade(dial.widget, 0.6);
fade(cap, 0.7);
fade(act, 0.6);
}
text(end1, (640, 336), "Four dimensions, watched from three.");
display(end1); size(end1, 44); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 416), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
dominos-one-push
The whole domino story in one film, from 3Blue1Brown’s 2017 experiments. ACT I a row places itself and one push crosses it (side on, so you watch each slab turn). ACT II why it works at all - the balance angle atan(t/h) it must climb past, the contact angle asin(s/h) it must reach. ACT III it doesn’t have to be a line: a spiral then a heart, seen from above, each placed one domino at a time. ACT IV the proof - twelve chains swept over gap x per-impact loss, where a gap taller than the domino and a too-lossy surface both kill the wave. Nothing in the file sets the speed; it emerges.
// dominos-one-push — the whole domino story in one film.
//
// Built from 3Blue1Brown's 2017 `dominos/` material, which is not an animation of falling
// dominos at all: it is 19 real experiments (frame numbers at 1000–5000 fps, one per tap)
// measuring how fast a toppling wave travels, plus a geometry scene showing how far a
// domino must tip to reach the next one. That geometry is the whole model here.
//
// ACT I a row places itself, then one push crosses it (`dominos`, side on)
// ACT II why it works at all — balance angle, contact angle
// ACT III it doesn't have to be a line: a spiral, then a heart (`dominopath`, top down)
// ACT IV the proof — twelve chains over spacing x loss, some of which die (`sweep`)
//
// Every domino is a rigid slab pivoting on its leading base edge: it must be pushed past
// atan(t/h) before gravity helps, and it reaches its neighbour at asin(s/h). Contact is
// sustained, so the whole leaning group drives the wave. Nothing in this file sets the
// wave's speed — it emerges from spacing, height, thickness and the per-impact loss.
// Lengths are in millimetres, so the experiment's own numbers go straight in.
//
// manic examples/dominos-one-push.manic
title("Dominos — One Push");
canvas("16:9");
template("black");
bloom(0.36, 0.52, 26);
// ---- the room: a dark tabletop, light falling from above ----
shader(room) {
let x = (u - 0.5) * asp;
let y = v - 0.5;
let d = sqrt(x*x + y*y);
let pool = 1.0 - smoothstep(0.06, 0.78, d);
let grain = 0.5 + 0.5*fbm(u*9.0, v*9.0);
let hue = 214 - 8.0*pool;
let sat = 0.55 - 0.25*pool;
let val = 0.012 + 0.055*pool + 0.012*grain*pool;
}
z(room, -10);
// ---- type ----
text(ttl, (640, 96), "Dominos — One Push");
display(ttl); size(ttl, 46); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 152), "and nothing here decides how fast it travels");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 656), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(note, (640, 618), ""); display(note); size(note, 19); color(note, gold); hidden(note);
// ---- ACT I — the row (side on: you can watch each slab turn) ----
dominos(row, (470, 386), 16, 45, 7.5438, 9.38, 2, 0.62, 7);
color(row, cyan);
color(row.ground, dim); // the table is furniture, not neon
color(row.d0, gold);
hidden(row.dominos);
framebox(mark, row.d0, 7); color(mark, gold); untraced(mark);
// ---- ACT II — the geometry, beside the row ----
equation(contact, (988, 330), `\theta_c=\arcsin\frac{s}{h}=12^{\circ}`, 27);
color(contact, gold); hidden(contact);
equation(balance, (988, 404), `\theta_b=\arctan\frac{t}{h}=9.5^{\circ}`, 27);
color(balance, magenta); hidden(balance);
text(geo1, (988, 462), "tip past 9.5° or it stands back up");
display(geo1); size(geo1, 18); color(geo1, dim); hidden(geo1);
text(geo2, (988, 492), "reach 12° and the next one goes");
display(geo2); size(geo2, 18); color(geo2, dim); hidden(geo2);
// ---- ACT III — the same physics, any shape (top down) ----
dominopath(spiral, (640, 380), 12.6, 12.6, "t*cos(t)", "t*sin(t)", 92, (1.4, 18.6), 45, 7.5438, 1.7, 0.7, 8);
hidden(spiral.dominos); untraced(spiral.path);
dominopath(heart, (640, 366), 12.2, 12.2,
"16*sin(t)^3", "13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)", 56, (0, 6.2832), 45, 7.5438, 1.7, 0.7, 8);
color(heart, magenta);
hidden(heart.dominos); untraced(heart.path);
// ---- ACT IV — twelve chains: gap across, per-impact loss down ----
dominos(cellrow, (0, 0), 10, 45, 7.5438, 9.38, 1.05, 0.6, 6); hidden(cellrow);
sweep(grid, cellrow, spacing, (9, 46), transfer, (0.95, 0.35), (640, 404), 4, 3, 250, 128, 0, 0);
hidden(grid);
// ============================ ACT I ============================
show(ttl, 0.8);
show(sub, 0.7);
show(cap, 0.3);
wait(0.5);
say(cap, "Sixteen slabs, forty-five millimetres tall, nine point four apart.");
show(row.ground, 0.5);
stagger(0.055) {
for i in 0..16 {
show(row.d{i}, 0.22);
}
}
wait(0.4);
draw(mark, 0.5);
say(cap, "One nudge, on that one.");
pulse(row.d0, 0.6);
wait(0.3);
par {
fade(ttl, 0.8);
fade(sub, 0.8);
}
run(row, 4.4);
wait(0.5);
say(cap, "The wave crossed the row. Its speed was never written down.");
wait(1.6);
// ============================ ACT II ============================
say(cap, "Each slab has to climb past its own balance angle before gravity takes over.");
show(balance, 0.7);
show(geo1, 0.5);
wait(1.8);
say(cap, "Then it only has to reach the next one — twelve degrees, for these dominos.");
show(contact, 0.7);
show(geo2, 0.5);
wait(2.2);
say(cap, "Two angles, and the whole cascade follows.");
wait(1.8);
// ============================ ACT III ============================
par {
fade(row, 0.8);
fade(mark, 0.5);
fade(contact, 0.7);
fade(balance, 0.7);
fade(geo1, 0.5);
fade(geo2, 0.5);
}
say(cap, "Nothing about that argument needs a straight line. Seen from above —");
draw(spiral.path, 1.3, smooth);
show(note, 0.4);
say(note, "r = t");
stagger(0.016) {
for i in 0..92 {
show(spiral.d{i}, 0.16);
}
}
wait(0.5);
say(cap, "Ninety-two dominos on a spiral, standing at equal spacing along the curve.");
run(spiral, 5.4);
wait(0.9);
par { fade(spiral, 0.8); fade(spiral.path, 0.6); }
say(cap, "Change the formula. Keep the physics.");
draw(heart.path, 1.1, smooth);
say(note, "x = 16 sin³t, y = 13 cos t − 5 cos 2t − 2 cos 3t − cos 4t");
size(note, 17);
stagger(0.022) {
for i in 0..56 {
show(heart.d{i}, 0.18);
}
}
wait(0.4);
say(cap, "A closed curve, so the wave runs all the way round and meets where it began.");
run(heart, 5.0);
wait(1.4);
// ============================ ACT IV ============================
par {
fade(heart, 0.8);
fade(heart.path, 0.6);
fade(note, 0.5);
}
say(cap, "So what does set the speed? Only the geometry — here it is, twelve times over.");
par {
show(grid.chrome, 0.6);
show(grid.headers, 0.7);
}
show(grid.cells, 0.9);
wait(0.5);
run(grid, 7.5);
wait(0.8);
say(cap, "Wider gaps run faster — until the gap is taller than the domino. Then nothing arrives.");
wait(2.2);
say(cap, "Bottom left dies too: that surface loses too much at every impact.");
wait(2.4);
// ============================ ENDCARD ============================
par {
fade(grid, 0.9);
fade(cap, 0.7);
}
text(end1, (640, 344), "One push. The rest is geometry.");
display(end1); size(end1, 46); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 424), "— manic");
display(end2); size(end2, 26); color(end2, cyan); hidden(end2);
show(end1, 0.9);
show(end2, 0.7);
wait(2.4);
dominos-any-shape
Dominos v2 — the same slab physics standing along ANY parametric curve, seen from above (a standing
domino is a short bar, a fallen one a long bar lying forward). A spiral, a heart and a 1:2 lissajous,
each PLACED ONE BY ONE with a staggered loop over the run’s own pieces, then toppled with one push.
count sets the spacing, so more dominos means tighter gaps and a slower wave; closed curves topple
all the way round. Nothing sets the wave speed - it emerges from the geometry.
// dominos-any-shape — dominos v2: stand them along ANY curve, place them one by one,
// then push the first one over.
//
// v1 (`examples/dominos.manic`) is the side view of a straight row, built from
// 3Blue1Brown's 2017 domino experiments. This is the same slab physics seen from
// ABOVE — where a standing domino is a short bar and a fallen one is a long bar lying
// forward along the path — which is what makes an arbitrary arrangement legible.
//
// The shape is a `param`-style formula pair, so anything you can write, you can topple:
// a spiral, a heart, a figure of eight. `count` sets the spacing (equal arc length), so
// asking for more dominos tightens the gaps and slows the wave; the physics is unchanged
// (balance angle atan(t/h), contact at asin(s/h), sustained contact carrying the wave)
// and nothing sets the speed — it emerges from the geometry.
//
// The placing-one-by-one beat needs no new vocabulary: the run's pieces are ordinary
// entities `{id}.d{i}`, so `stagger` over a `for` loop lays them down in order.
//
// manic examples/dominos-any-shape.manic
title("Dominos, any shape you like");
canvas("16:9");
template("black");
bloom(0.3, 0.5, 20);
text(head, (640, 70), "Dominos, any shape you like");
display(head); size(head, 34); bold(head); color(head, fg); hidden(head);
text(cap, (640, 660), ""); display(cap); size(cap, 23); color(cap, dim); hidden(cap);
text(shape, (640, 618), ""); display(shape); size(shape, 20); color(shape, gold); hidden(shape);
// ---- three arrangements, same physics ----
// a spiral: r = t, so the gap between turns stays constant
dominopath(spiral, (640, 372), 13, 13, "t*cos(t)", "t*sin(t)", 96, (1.4, 19.2), 45, 7.5438, 1.7, 0.7, 8);
hidden(spiral.dominos); untraced(spiral.path); // pieces fade in; the guide is armed for draw-on
// a heart — the classic parametric one, closed, so the wave runs all the way round
dominopath(heart, (640, 356), 12.5, 12.5,
"16*sin(t)^3", "13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)", 58, (0, 6.2832), 45, 7.5438, 1.7, 0.7, 8);
hidden(heart.dominos); untraced(heart.path); // pieces fade in; the guide is armed for draw-on
// a figure of eight: a lissajous with a 1:2 frequency ratio
dominopath(eight, (640, 372), 300, 150, "sin(t)", "sin(2*t)", 84, (0, 6.2832), 45, 7.5438, 1.7, 0.72, 8);
hidden(eight.dominos); untraced(eight.path); // pieces fade in; the guide is armed for draw-on
// ---- Act 1: the spiral, laid down one domino at a time ----
show(head, 0.6);
show(cap, 0.3);
say(cap, "Ninety-six dominos, standing along a spiral. Watch them go down one by one.");
draw(spiral.path, 1.2, smooth);
show(shape, 0.4);
say(shape, "r = t");
stagger(0.017) {
for i in 0..96 {
show(spiral.d{i}, 0.16);
}
}
wait(0.5);
say(cap, "One push at the middle, and the wave winds outward. Nothing here sets its speed.");
run(spiral, 5.5);
wait(0.8);
// ---- Act 2: the same physics, a heart ----
par { fade(spiral, 0.7); fade(spiral.path, 0.5); }
say(cap, "Change the formula, keep the physics. A closed curve topples all the way round.");
draw(heart.path, 1.0, smooth);
say(shape, "x = 16 sin³t, y = 13 cos t − 5 cos 2t − 2 cos 3t − cos 4t");
size(shape, 17);
stagger(0.022) {
for i in 0..58 {
show(heart.d{i}, 0.18);
}
}
wait(0.4);
run(heart, 5.0);
wait(0.8);
// ---- Act 3: a figure of eight, and the wave crosses its own middle ----
par { fade(heart, 0.7); fade(heart.path, 0.5); }
say(cap, "A lissajous at 1:2 — the run crosses itself, and the wave passes straight through.");
say(shape, "x = sin t, y = sin 2t");
size(shape, 20);
draw(eight.path, 1.0, smooth);
stagger(0.018) {
for i in 0..84 {
show(eight.d{i}, 0.16);
}
}
wait(0.4);
run(eight, 5.0);
wait(0.6);
say(cap, "Same slab, same contact angle, same emergent speed — only the path changed.");
wait(2.0);
dominos
A toppling wave, and what sets its speed — from 3Blue1Brown’s 2017 domino EXPERIMENTS (19 data
files of frame numbers at 1000-5000 fps). Each domino is a rigid slab pivoting on its base edge, so
it must tip past its balance angle atan(t/h) before gravity helps and reaches its neighbour at the
contact angle asin(spacing/height); contact is SUSTAINED, so the leaning group carries the wave.
Nothing sets the speed — it emerges. Lengths are in millimetres, so the experiment’s own numbers go
in (45 tall, 7.5438 thick, 9.38 apart). Act 2 is the same row rebuilt by sweep over spacing x
per-impact loss: twelve chains, some of which never finish.
// dominos — a toppling wave, and what sets its speed.
//
// From 3Blue1Brown's 2017 `dominos/domino_play.py`, which is not an animation of falling
// dominos at all: it plots 19 real experiments (frame numbers at 1000–5000 fps, one per
// tap) to measure how fast the toppling wave travels, and a `Test` scene that draws the
// geometry — how far a domino must tip before it reaches the next one.
//
// That geometry is the whole model. Each domino is a rigid slab pivoting on its leading
// base edge, so it must be pushed past its balance angle atan(t/h) before gravity helps,
// and it reaches its neighbour at the contact angle asin(spacing/height) — the arc 3b1b
// draws. Contact is SUSTAINED: the faller leans on the next one and keeps driving it, so
// the weight of the whole leaning group carries the wave. Nothing here sets the speed
// directly: it emerges from spacing, height, thickness and the per-impact loss.
//
// Lengths are in millimetres, so the experiment's own numbers go straight in — 45 mm
// tall, 7.5438 mm thick, 9.38 mm apart is 3b1b's main dataset.
//
// manic examples/dominos.manic
title("Dominos — the wave nobody sets the speed of");
canvas("16:9");
template("black");
// ---- the row: real domino numbers, tagged so core verbs reach it ----
dominos(row, (470, 300), 14, 45, 7.5438, 9.38, 2, 0.6, 6);
color(row, cyan); // one verb, the whole row
color(row.d0, gold); // ...or one piece
framebox(first, row.d0, 7); color(first, gold); untraced(first);
text(head, (640, 74), "One push, fourteen dominos");
display(head); size(head, 34); bold(head); color(head, fg); hidden(head);
text(cap, (640, 654), ""); display(cap); size(cap, 24); color(cap, dim); hidden(cap);
equation(geo, (950, 250), `\theta_c=\arcsin\!\frac{s}{h}=12^{\circ}`, 26);
color(geo, gold); hidden(geo);
equation(bal, (950, 320), `\theta_b=\arctan\!\frac{t}{h}=9.5^{\circ}`, 26);
color(bal, magenta); hidden(bal);
text(geonote, (950, 392), "tip past 9.5° or it stands back up");
display(geonote); size(geonote, 19); color(geonote, dim); hidden(geonote);
// ---- the grid: the same row rebuilt over spacing × per-impact loss ----
// `sweep` re-invokes the SAME constructor per cell, varying two of its own named
// parameters — so this grid is 12 independent chains, some of which die out.
dominos(cell, (0, 0), 10, 45, 7.5438, 9.38, 1.05, 0.6, 6); hidden(cell);
sweep(grid, cell, spacing, (9, 40), transfer, (0.9, 0.45), (640, 430), 4, 3, 250, 130, 0, 0);
hidden(grid);
text(gridhead, (640, 138), "Wider gaps run faster — until nothing arrives");
display(gridhead); size(gridhead, 27); bold(gridhead); color(gridhead, fg); hidden(gridhead);
text(gridnote, (640, 646), "");
display(gridnote); size(gridnote, 21); color(gridnote, dim); hidden(gridnote);
// ---- Act 1: one row falls ----
show(head, 0.6);
show(cap, 0.3);
say(cap, "Fourteen slabs, one nudge. Nothing in the file says how fast the wave travels.");
show(row, 0.7);
draw(first, 0.5);
wait(0.6);
run(row, 4.2);
wait(0.5);
say(cap, "Each slab has to tip past its own balance angle, then it reaches the next one.");
par { show(geo, 0.6); show(bal, 0.6); }
show(geonote, 0.5);
wait(2.6);
// ---- Act 2: the same row, swept over spacing and loss ----
par {
fade(row, 0.6);
fade(first, 0.4);
fade(cap, 0.4);
fade(geo, 0.5);
fade(bal, 0.5);
fade(geonote, 0.5);
fade(head, 0.5);
}
show(gridhead, 0.6);
show(gridnote, 0.3);
say(gridnote, "Twelve chains: gap across, per-impact loss down. Some never finish.");
par {
show(grid.chrome, 0.5);
show(grid.headers, 0.6);
}
show(grid.cells, 0.8);
wait(0.4);
run(grid, 7);
wait(0.6);
say(gridnote, "Speed is an outcome here, not a setting — the geometry decides it.");
wait(2.2);
cloud-ink-pair
Another @yuruyurau art-tweet in ONE cloud, staged on PAPER instead of in the dark: two fish
circling in ink. The whole trick is (i%2)*3 — every other point phase-shifted by 3 radians, so
one formula draws TWO animals orbiting a shared centre. template("paper"), no bloom, and the
ink pools dark along the bodies (alpha falls off with d) and dries to nothing at the fin
tips; p5’s semi-transparent stroke(w,96) was always ink. The near-vertical dotted streaks are
the original’s own 77 sin(e/2) passing through zero — its +1e-4 guard bounds the blow-up
instead of removing it, so they fall like rain. Pure in (i, t), so it scrubs.
// cloud-ink-pair — another @yuruyurau creature in ONE `cloud`, reimagined as a brush
// study: two fish circling on paper. The reference is a tweet-sized golf:
// k = 5cos(i/44), e = y/2-15, d = mag(k,e)/3, c = d/2 - t/3 + (i%2)·3, y = i/253
// point( (79 + d² + k²)·sin c + 200 + d³/4·cos(3t - d²/4) ,
// 99cos(c/2) + 4sin 2k + y/(77 sin(e/2) + 1e-4)·k·e + 200 )
// The whole trick is `(i%2)·3`: every other point is phase-shifted by 3 radians, so ONE
// formula draws TWO animals — a mirrored pair, orbiting a shared centre as `c` turns.
// The near-vertical dotted streaks are the original's own doing: `77 sin(e/2)` passes
// through zero, and the author's `+1e-4` guard bounds the blow-up instead of removing
// it. We keep the guard and the streaks; they fall like rain behind the pair.
//
// So this one is staged on PAPER rather than in the dark: `template("paper")`, no bloom,
// ink pooling dark along the bodies (`alpha` falls off with `d`) and drying to almost
// nothing at the fin tips. p5's semi-transparent `stroke(w,96)` was always ink.
//
// Faithful notes: p5's `mag` is `hypot`, `%` is `mod`, `**` is `^`; the p5 draw loop
// advances t by PI/80 per FRAME, so a frame-rate-free `t*1.0` stands in for it. Pure in
// (i, t) — it scrubs, seeks and records exactly, which the p5 original cannot do.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau).
//
// manic examples/cloud-ink-pair.manic
title("Two, in ink — a pair from one formula");
canvas("square");
template("paper");
// ---- the pair — `mod(i,2)` splits one formula into two animals ----
// Framing is the original's own, scaled: p5 draws into 400×400 about (200,200), so
// ×2.7 about (540,540) reproduces it at 1080, rain-streaks running off frame and all.
cloud(pair, 10000, #14141a, 0.95) {
let yy = i / 253.0;
let k = 5.0 * cos(i / 44.0);
let e = yy / 2.0 - 15.0;
let d = hypot(k, e) / 3.0;
let T = t * 1.0;
let c = d/2.0 - T/3.0 + mod(i, 2) * 3.0;
let px = (79.0 + d*d + k*k) * sin(c) + (d^3)/4.0 * cos(T*3.0 - d*d/4.0);
let py = 99.0*cos(c/2.0) + 4.0*sin(k*2.0) + yy/(77.0*sin(e/2.0) + 0.0001) * k * e;
let x = 540 + px * 2.7;
let y = 540 + py * 2.7;
// ink pools along the body, dries out toward the fins
let alpha = clamp(0.9 - d * 0.07, 0.16, 0.92);
let r = 1.3;
}
// ---- annotations ----
caption(head, "Two, in ink", (540, 96), 34); hidden(head);
caption(sub, "one formula, split by mod(i,2)", (540, 152), 21); hidden(sub);
equation(eq, (540, 946), `c=\tfrac{d}{2}-\tfrac{t}{3}+(i\bmod 2)\cdot 3`, 25); hidden(eq);
caption(lab, "manic", (540, 1006), 18); hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(24);
cloud-krill
Another @yuruyurau art-tweet in ONE cloud: a compact, densely bristled swimmer, pale and
cold-lit on a specimen plate. It is the SAME golf as cloud-crinoid with three constants
changed — y/5-13 for y/4-16, mag-4 for mag-5, and cos for sin in the last term —
which pulls the crinoid’s long sweeping arms into something stubby and coiled, breathing out of
phase with its own sweep. These tweet-sized formulas are a parameter space you can walk, not a
single drawing. Pure in (i, t), so it scrubs; the p5 original can’t.
// cloud-krill — another @yuruyurau creature in ONE `cloud`: a compact, densely bristled
// swimmer, pale and cold-lit. It is the same golf as `cloud-crinoid.manic` with three
// constants changed — worth knowing, because these tweet-sized formulas are a parameter
// space you can walk, not a single drawing:
//
// crinoid e = y/4 - 16 d = mag(k,e) - 5 + d²/3·sin(t - d²/7)
// this one e = y/5 - 13 d = mag(k,e) - 4 + d²/3·cos(t - d²/9)
//
// Everything else is identical — k = 4cos(i/29), c = d - t/3, y = i/295, and
// point( (d²/0.7 - 2k² + y)·cos c + 200 ,
// 3sin 2k + cos(y)/k + (y/9)k(3 + sin(9e - 3d + t)) + 79sin(c/3) + … + 200 )
// Shortening the spine (`-4`) and slowing the body taper (`y/5`) pulls the long sweeping
// arms in: where the crinoid fans wide, this is compact, dense and coiled — a stubbier
// relative on the same skeleton. The phase change from `sin` to `cos` in the last term
// re-times the breathing against the sweep, so the two never move alike.
//
// Staged as a specimen plate rather than a scene: no reef wall, no warm stone — a cold
// dark ground, a pale bone-blue body, and the three changed constants typeset below it.
//
// Faithful notes: p5's `mag` is `hypot`, `**` is `^`; the p5 draw loop advances t by
// PI/60 per FRAME, so a frame-rate-free `t*1.333` stands in for it (the crinoid's
// PI/40 became `t*2.0`, so the two run at the same relative pace). `cos(y)/k` keeps its
// division-by-almost-zero flecks. Pure in (i, t), so it scrubs, seeks and records.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau).
//
// manic examples/cloud-krill.manic
title("Krill — ten thousand points, one closed form");
canvas("square");
template("black");
bloom(0.3, 0.55, 22);
// ---- a cold, near-empty field: this is a plate, not a habitat ----
shader(field) {
let x = (u - 0.5) * asp;
let y = v - 0.5;
let d = sqrt(x*x + y*y);
let vig = 1.0 - 0.9*smoothstep(0.1, 0.75, d);
let hue = 214;
let sat = 0.5;
let val = 0.028 * vig + 0.008;
}
z(field, -10);
// ---- the animal — swept envelope is 222×459, so ×2.2 about (540,540) fills the plate ----
cloud(body, 10000, #ffffff, 0.34) {
let yy = i / 295.0;
let k = 4.0 * cos(i / 29.0);
let e = yy / 5.0 - 13.0;
let d = hypot(k, e) - 4.0;
let T = t * 1.333;
let c = d - T / 3.0;
let px = (d*d/0.7 - k*k*2.0 + yy) * cos(c);
let py = 3.0*sin(k*2.0) + cos(yy)/k + yy/9.0*k*(3.0 + sin(e*9.0 - d*3.0 + T)) + 79.0*sin(c/3.0) + (d*d)/3.0*cos(T - d*d/9.0);
let x = 540 + px * 2.2;
let y = 540 + py * 2.2;
// bone-blue: pale at the spine, cooling toward the barb tips
let hue = mod(206.0 + d * 2.2, 360);
let sat = clamp(0.06 + d * 0.05, 0.04, 0.5);
let val = clamp(0.74 + 0.26*sin(e*9.0 - d*3.0 + T), 0.36, 1.0);
let r = 1.25;
}
glow(body, 1);
// ---- annotations ----
caption(head, "Krill", (540, 96), 34); hidden(head);
caption(sub, "ten thousand points, one closed form", (540, 152), 21); hidden(sub);
equation(eq, (540, 950), `k=4\cos\tfrac{i}{29},\quad d=\mathrm{mag}\!\left(k,\tfrac{y}{5}-13\right)-4,\quad c=d-\tfrac{t}{3}`, 24); hidden(eq);
caption(lab, "manic", (540, 1010), 18); hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(24);
cloud-crinoid
Another @yuruyurau art-tweet in ONE cloud, reimagined as a gold feather star combing the
current: 10,000 points trace a curved spine with dozens of hooked barbs that sweep and re-comb
as the angle c = d - t/3 turns, over d = mag(4cos(i/29), y/4-16) - 5. The shader behind it
is the reef wall it clings to (mottled warm stone); the arms run bone at the spine → amber at the
barb tips, and the original’s cos(y)/k division-by-almost-zero survives as four drifting
flecks a frame. Pure in (i, t), so it scrubs; the p5 original can’t.
// cloud-crinoid — another @yuruyurau creature in ONE `cloud`, reimagined as a gold
// feather star combing the current. The reference is a tweet-sized golf:
// k = 4cos(i/29), e = y/4-16, d = mag(k,e)-5, c = d-t/3, y = i/295
// point( (d²/0.7 - 2k² + y)·cos c + 200 ,
// 3sin 2k + cos(y)/k + (y/9)k(3+sin(9e-3d+t)) + 79sin(c/3) + d²/3·sin(t-d²/7) + 200 )
// One curved spine with dozens of hooked barbs that sweep and re-comb as `c` turns —
// which is what a crinoid does for a living: perch on rock, fan its arms, strain the
// water. So the `shader` behind it is the reef wall it clings to (mottled warm stone),
// the arms run bone at the spine → amber at the barb tips, and `cos(y)/k` keeps its
// division-by-almost-zero spikes: four flecks a frame, drifting plankton.
//
// Faithful notes: p5's `mag` is `hypot`; `**` is `^`; the p5 draw loop advances t by
// PI/40 per FRAME, so a frame-rate-free `t*2.0` stands in for it. Pure in (i, t) —
// it scrubs, seeks and records exactly, which the p5 original cannot do.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau).
//
// manic examples/cloud-crinoid.manic
title("Crinoid — combing the current");
canvas("square");
template("black");
bloom(0.34, 0.55, 26);
// ---- the reef wall it perches on: mottled warm stone, darker toward the edges ----
shader(wall) {
let x = (u - 0.5) * asp;
let y = v - 0.5;
let d = sqrt(x*x + y*y);
let grain = 0.5 + 0.5*fbm(u*7.0, v*7.0);
let mott = 0.5 + 0.5*fbm(u*2.2 + 3.0, v*2.2);
let vig = 1.0 - 0.85*smoothstep(0.15, 0.72, d);
let hue = 28 + 10.0*mott;
let sat = 0.34 - 0.12*grain;
let val = (0.055 + 0.055*mott + 0.018*grain) * vig + 0.012;
}
z(wall, -10);
// ---- the animal — the yuruyurau golf, re-lit and framed ----
// The swept envelope of the formula is 315×185 wide over a full cycle of `c`, so
// scale 2.95 about (556, 435) centres it in the square at every t, not just at t=0.
cloud(arms, 10000, #ffffff, 0.34) {
let yy = i / 295.0;
let k = 4.0 * cos(i / 29.0);
let e = yy / 4.0 - 16.0;
let d = hypot(k, e) - 5.0;
let T = t * 2.0;
let c = d - T / 3.0;
let px = (d*d/0.7 - k*k*2.0 + yy) * cos(c);
let py = 3.0*sin(k*2.0) + cos(yy)/k + yy/9.0*k*(3.0 + sin(e*9.0 - d*3.0 + T)) + 79.0*sin(c/3.0) + d*d/3.0*sin(T - d*d/7.0);
let x = 556 + px * 2.95;
let y = 435 + py * 2.95;
// bone along the spine (small d) → amber where the barbs thin out (large d)
let hue = mod(44.0 - d * 1.1, 360);
let sat = clamp(0.10 + d * 0.045, 0.06, 0.62);
let val = clamp(0.72 + 0.28*sin(e*9.0 - d*3.0 + T), 0.34, 1.0);
let r = 1.25;
}
glow(arms, 1);
// ---- annotations ----
caption(head, "Crinoid", (540, 96), 34); hidden(head);
caption(sub, "one formula, ten thousand points", (540, 152), 21); hidden(sub);
equation(eq, (540, 946), `k=4\cos\tfrac{i}{29},\quad d=\mathrm{mag}\!\left(k,\tfrac{y}{4}-16\right)-5,\quad c=d-\tfrac{t}{3}`, 25); hidden(eq);
caption(lab, "manic", (540, 1006), 18); hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(24);
cloud-plumes
Another @yuruyurau art-tweet in ONE cloud: 30,000 points in two mirrored layers (mod(i,2)) drift
into flowing frond/plume forms and morph over time — a polar plot (radius q, angle c), hue-gradient
coloured and bloomed on a 9:16 Short. Pure in (i, t), so it scrubs; the p5 original can’t.
// cloud-plumes — another @yuruyurau art-tweet in ONE `cloud`: 30,000 points in
// two mirrored layers (`mod(i,2)`) drift into flowing frond/plume forms and morph
// over time. A polar plot — radius `q`, angle `c` — coloured per point and
// bloomed from the centre on a 9:16 Short. `mag(k,e)^2` becomes `k*k+e*e`, and
// the canvas `w` (=400) is folded into the constant `i/1200`.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau) — a prolific poster of
// tiny p5.js/dwitter art formulas. Our own hue'd, annotated take. Pure in (i, t),
// so it scrubs and records; the p5 original can't.
//
// manic examples/cloud-plumes.manic
title("Two fronds from one formula");
canvas("9:16");
template("black");
cloud(fronds, 30000, #ffffff, 0.6) {
let m = mod(i, 2) * 3; // two layers: 0, 3
let k = 14*cos(i/39);
let e = i/1200 - 13;
let d = (k*k + e*e)/59 + 1;
let q = 89 - sin(k)*d + k*(8/d + sin(d*3 + e/9 - t));
let c = d*0.45 - sin(t - d)/8 - t/8 + m;
let px = q*sin(c);
let py = (q + 40 + 30*sin(c*2 + m))*cos(c);
let grow = tanh(t*0.5 + 0.12);
let x = 540 + px * 2.0 * grow;
let y = 960 + py * 2.0 * grow;
let hue = mod(m*70 + i*0.03 + t*15, 360);
}
// ---- textbook annotations ----
caption(head, "Two fronds from one formula", (540, 138), 36);
caption(sub, "30,000 points, no simulation", (540, 206), 22);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\sin c,\;\; (q{+}\Delta)\cos c)`, 30);
caption(lab, "a polar plot: radius q, angle c, per point", (540, 1786), 20);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(25);
cloud-shells
A tiny art-tweet by @yuruyurau, reimagined in ONE cloud: 10,000 points in three layers (mod(i,3))
placed by a polar formula (radius q, angle c), hue-gradient coloured and bloomed out of the centre on
a 9:16 Short. Every operator in the 200-char p5 original mapped straight across — and unlike p5 it
scrubs and records (pure in i, t).
// cloud-shells — a dwitter-style art-tweet reimagined in manic: ONE `cloud` of
// 10,000 points in three layers (`mod(i,3)`), placed by a polar formula — radius
// `q`, angle `c` — then coloured per point (a hue gradient per form) and bloomed
// out of the centre. Rebuilt as a textbook Short: every point a pure function of
// (i, t), so it scrubs and records.
//
// Original idea by @yuruyurau (https://x.com/yuruyurau) — a prolific poster of
// these tiny p5.js/dwitter art formulas. This is our own hue'd, annotated take.
//
// manic examples/cloud-shells.manic
title("One formula, ten thousand points");
canvas("9:16");
template("black");
cloud(swirl, 10000, #ffffff, 0.72) {
let m = mod(i, 3) * 4; // three layers: 0, 4, 8
let k = 9 * cos(i / 81);
let e = i / 461 - 11;
let d = hypot(k, e)^4 / 40000 + 1.5 + sin(t/2 + m)/4;
let q = 89 - e*sin(k) + k*(4 + 2*sin(d*9 + e/9 - t));
let c = d + sin(t - d*4)/9 - t/9 + m;
// raw shell coords (centred at 0), then bloom + scale onto the 1080x1920 frame
let qx = q*cos(c);
let qy = (q + 30)*sin(c);
let grow = tanh(t * 0.5 + 0.12); // blooms from the centre
let x = 540 + qx * 2.9 * grow;
let y = 980 + qy * 2.9 * grow;
let r = 1.4;
// colour: a gradient along each form (index) with the three layers offset, all
// slowly cycling — every point its own hue
let hue = mod(m * 46 + i * 0.05 + t * 18, 360);
}
// ---- textbook annotations ----
caption(head, "One formula, 10,000 points", (540, 132), 40);
caption(sub, "a 200-char art-tweet, rebuilt in manic", (540, 202), 24);
hidden(head);
hidden(sub);
equation(eq, (540, 1706), `p = (q\cos c,\; q\sin c)`, 44);
caption(lab, "a polar plot: radius q, angle c, per point", (540, 1784), 22);
hidden(eq);
hidden(lab);
show(head);
wait(1.4);
show(sub);
wait(2.6);
show(eq);
show(lab);
wait(17);
popkorn-field
6000 points, one closed-form formula each, on a 9:16 Short. The cloud primitive places every
point by a function of its index i and live time t, blooming out of the centre (a tanh
envelope) while each point cycles its own hue — a moving field that still scrubs and records exactly.
// popkorn-field — a 6000-point parametric field for Shorts (9:16). One closed-
// form formula per point, animated by live time `t`: it blooms out of the centre
// over the first few seconds (a `tanh` growth envelope), then keeps evolving,
// while each point cycles its own hue. This is the `cloud` primitive — N points
// placed by formulas of the index `i` and the clock `t`, re-evaluated every
// frame, yet still a pure function of t (it scrubs and records exactly).
//
// The maths is domain-neutral; `cloud` knows nothing about what it draws. Note
// `hypot(a,b)`, `mod(...)` for the hue wrap, and the `0.5*(s + abs(s))` idiom for
// `max(0, s)` (min/max are reductions in manic, not 2-arg functions).
//
// manic examples/popkorn-field.manic
title("popkorn field — 6000 points, one formula each");
canvas("9:16");
template("black");
cloud(dust, 6000, #ffffff, 0.5) {
let a = 4 * cos(i / 21);
let b = i / 1880 - 20;
let k = hypot(a, b);
let S = 3 * sin(2 * a) + 0.3 / a
+ sin(i / 4465) * a * (9 + 2 * sin(b * 14 - k * 3 + 2 * t));
// raw field, centred near (200,120) at ~400px scale in the original
let fx = S + 50 * cos(k - t) + 200;
let fy = S * sin(k - t) + k * 39 - 475;
// bloom from the centre: 0.15 → ~1 over the first several seconds, so the
// full structure only resolves "after some point", then keeps drifting
let grow = tanh(t * 0.35 + 0.15);
let x = 540 + (fx - 205) * 3.3 * grow;
let y = 960 + (fy - 250) * 3.3 * grow;
// radius 1 where a*a>15 else 0.5, scaled up for the taller canvas
let s = sign(a * a - 15);
let r = (0.5 * (1 + 0.5 * (s + abs(s)))) * (2.5 + grow);
// each point its own hue, the whole wheel cycling over time
let hue = mod(i * 0.05 + t * 30, 360);
}
wait(16);
astronomical-watch
A textbook orrery from REAL SVG assets (Twemoji planets, svg() import): the Sun + six planets are
imported vector art, each turned around the Sun with inner planets faster (Kepler), over a cloud
starfield that twinkles via per-point alpha. The eight real moon-phase glyphs run along the bottom
and the orbit + Kepler maths derive on the left — real shapes, real astronomy, live.
// astronomical-watch — a textbook orrery: a clockwork solar system built from
// REAL SVG assets (Twemoji, vendored by scripts/fetch-svg-assets.sh). The Sun
// and eight planets are imported vector art; each planet is swept around the Sun by
// `turn` about the shared pivot, inner planets faster (Kepler's third law). The
// starfield is a `cloud` (per-point `alpha` twinkle), and the eight real
// moon-phase glyphs run along the bottom. The maths is derived on the left.
//
// # assets first (one-time): scripts/fetch-svg-assets.sh
// manic examples/astronomical-watch.manic
title("An orrery — a clockwork solar system");
canvas(1280, 720);
template("black");
// ---- starfield (a dense cloud; drifts, twinkles, and has depth) ----
cloud(stars, 320, #dfeaff, 1) {
let x = 1280 * noise(i, 1);
let y = mod(720 * noise(i, 5) + t * 8, 720); // slow drift down + wrap
let sz = noise(i, 9);
let r = 1.1 + sz * sz * 2.6; // 1.1..3.7 — depth, all visible
let alpha = 0.62 + 0.34 * sin(t * (1.0 + noise(i, 3)) + i * 7); // 0.28..0.96
}
// ---- orbit rings (faint, dashed), centred on the Sun at (830, 300) ----
circle(r1, (830, 300), 40); outlined(r1); dashed(r1); color(r1, #2a2a4e);
circle(r2, (830, 300), 64); outlined(r2); dashed(r2); color(r2, #2a2a4e);
circle(r3, (830, 300), 90); outlined(r3); dashed(r3); color(r3, #2a2a4e);
circle(r4, (830, 300), 118); outlined(r4); dashed(r4); color(r4, #2a2a4e);
circle(r5, (830, 300), 152); outlined(r5); dashed(r5); color(r5, #2a2a4e);
circle(r6, (830, 300), 192); outlined(r6); dashed(r6); color(r6, #2a2a4e);
circle(r7, (830, 300), 232); outlined(r7); dashed(r7); color(r7, #2a2a4e);
circle(r8, (830, 300), 270); outlined(r8); dashed(r8); color(r8, #2a2a4e);
// ---- the Sun + all eight planets (real imported SVGs) ----
svg(sun, (830, 300), "asset:svg/emoji/sun.svg", 60);
svg(mercury, (870, 300), "asset:svg/emoji/mercury.svg", 15);
svg(venus, (894, 300), "asset:svg/emoji/venus.svg", 22);
svg(earth, (920, 300), "asset:svg/emoji/earth.svg", 26);
svg(mars, (948, 300), "asset:svg/emoji/mars.svg", 19);
svg(jupiter, (982, 300), "asset:svg/emoji/jupiter.svg", 40);
svg(saturn, (1022, 300), "asset:svg/emoji/saturn.svg", 46);
svg(uranus, (1062, 300), "asset:svg/emoji/uranus.svg", 28);
svg(neptune, (1100, 300), "asset:svg/emoji/neptune.svg", 28);
// ---- the lesson (left column; text/caption CENTRE on their point) ----
caption(head, "A clockwork solar system", (240, 54), 30);
caption(sub, "inner planets orbit faster", (240, 98), 20);
hidden(head);
hidden(sub);
equation(eq1, (240, 240), `\vec p = (R\cos\omega t,\; R\sin\omega t)`, 26);
text(lab1, (240, 292), "swept around the Sun");
hidden(eq1);
hidden(lab1);
equation(eq2, (240, 410), `T^{2} \propto R^{3}`, 34);
text(lab2, (240, 466), "far = slow (Kepler)");
hidden(eq2);
hidden(lab2);
// ---- the Moon's phases: eight real glyphs along the bottom ----
caption(moonlab, "the Moon's phases", (640, 620), 22);
svg(p1, (300, 668), "asset:svg/emoji/newmoon.svg", 42);
svg(p2, (405, 668), "asset:svg/emoji/waxingcrescent.svg",42);
svg(p3, (510, 668), "asset:svg/emoji/firstquarter.svg", 42);
svg(p4, (615, 668), "asset:svg/emoji/waxinggibbous.svg", 42);
svg(p5, (720, 668), "asset:svg/emoji/fullmoon.svg", 42);
svg(p6, (825, 668), "asset:svg/emoji/waninggibbous.svg", 42);
svg(p7, (930, 668), "asset:svg/emoji/lastquarter.svg", 42);
svg(p8, (1035, 668),"asset:svg/emoji/waningcrescent.svg",42);
hidden(moonlab);
hidden(p1); hidden(p2); hidden(p3); hidden(p4);
hidden(p5); hidden(p6); hidden(p7); hidden(p8);
// ---- run it: planets orbit (in parallel), the lesson reveals alongside ----
par {
turn(mercury, (830, 300), 4320, 24, linear); // 12 revolutions — fastest
turn(venus, (830, 300), 2520, 24, linear); // 7
turn(earth, (830, 300), 1620, 24, linear); // 4.5
turn(mars, (830, 300), 1080, 24, linear); // 3
turn(jupiter, (830, 300), 432, 24, linear); // 1.2
turn(saturn, (830, 300), 252, 24, linear); // 0.7
turn(uranus, (830, 300), 162, 24, linear); // 0.45
turn(neptune, (830, 300), 108, 24, linear); // 0.3 — slowest
seq {
show(head);
wait(0.9);
show(sub);
wait(1.4);
show(eq1); show(lab1);
wait(2.4);
show(eq2); show(lab2);
wait(2.2);
show(moonlab);
show(p1); show(p2); show(p3); show(p4);
show(p5); show(p6); show(p7); show(p8);
wait(4);
}
}
manic-promo
A generative promo from the cloud primitive alone: five particle swarms fly in and assemble
into words — MANIC (a cycling rainbow) at centre, with 3B1B, Manim, Animation and Generative in
the four corners. Each is from text("…"); the mid-assembly convergence storm is the money shot.
// manic-promo — a generative promo built from the `cloud` primitive alone.
// Five particle swarms fly in and assemble into words: MANIC at the centre,
// with 3B1B, Manim, Animation and Generative claiming the four corners. Each
// word is `cloud(...) from text("…")` — the glyphs are filled with points whose
// homes arrive as `hx`/`hy`; the block re-centres and scales that home to its
// slot, then blends the swarm in from a golden-angle scatter over time `t`.
// One primitive, five words, no art assets. Change the words and it just works.
//
// manic examples/manic-promo.manic
title("manic — generative animation, from a swarm");
canvas(1080, 1080);
template("black");
// --- centre: MANIC, big, a cycling rainbow ---------------------------------
cloud(manic, 2000, #ffffff, 0.96) from text("MANIC") {
let a = 0.5 * (1 + tanh((t - mod(i * 7, 29) * 0.04 - 1.0) * 2.2));
let px = (hx - 540) * 0.62 + 540;
let py = (hy - 540) * 0.62 + 540;
let sx = 540 + cos(i * 2.39996) * (420 + mod(i * 97, 260));
let sy = 540 + sin(i * 2.39996) * (420 + mod(i * 97, 260));
let x = sx * (1 - a) + px * a;
let y = sy * (1 - a) + py * a;
let r = 2.4;
let hue = mod(hx * 0.4 + t * 22, 360);
}
// --- four corners: the world manic plays in --------------------------------
cloud(tl, 780, #3b8ee0, 0.95) from text("3B1B") {
let a = 0.5 * (1 + tanh((t - 2.4) * 2.2));
let px = (hx - 540) * 0.34 + 250;
let py = (hy - 540) * 0.34 + 240;
let x = (250 + cos(i * 2.39996) * 460) * (1 - a) + px * a;
let y = (240 + sin(i * 2.39996) * 460) * (1 - a) + py * a;
let r = 2;
}
cloud(tr, 820, #46e2c8, 0.95) from text("Manim") {
let a = 0.5 * (1 + tanh((t - 2.7) * 2.2));
let px = (hx - 540) * 0.34 + 830;
let py = (hy - 540) * 0.34 + 240;
let x = (830 + cos(i * 2.39996) * 460) * (1 - a) + px * a;
let y = (240 + sin(i * 2.39996) * 460) * (1 - a) + py * a;
let r = 2;
}
cloud(bl, 1000, #f0a54e, 0.95) from text("Animation") {
let a = 0.5 * (1 + tanh((t - 3.0) * 2.2));
let px = (hx - 540) * 0.30 + 250;
let py = (hy - 540) * 0.30 + 840;
let x = (250 + cos(i * 2.39996) * 460) * (1 - a) + px * a;
let y = (840 + sin(i * 2.39996) * 460) * (1 - a) + py * a;
let r = 2;
}
cloud(br, 1050, #b06ef0, 0.95) from text("Generative") {
let a = 0.5 * (1 + tanh((t - 3.3) * 2.2));
let px = (hx - 540) * 0.30 + 830;
let py = (hy - 540) * 0.30 + 840;
let x = (830 + cos(i * 2.39996) * 460) * (1 - a) + px * a;
let y = (840 + sin(i * 2.39996) * 460) * (1 - a) + py * a;
let r = 2;
}
wait(12);
cloud-word
Type a word, get a particle swarm: cloud(...) from text("MANIC") fills the glyphs and hands
each point its home (hx,hy); the formulas fly the swarm in from all sides, hold the word, then
burst it apart, in a left-to-right rainbow. Change the word and it just works.
// cloud-word — type a word, get a particle swarm that flies in from all sides,
// assembles into the letters, holds, then bursts apart. The `cloud` primitive's
// `from text("…")` source fills the glyphs and hands each point its home as
// `hx`/`hy`; the block formulas fly those homes in and out over time `t`. Change
// the word and it just works — the end-user's creativity, not a hard-coded logo.
//
// manic examples/cloud-word.manic
title("cloud — a word from a swarm");
canvas("9:16");
template("black");
cloud(word, 1800, #ffffff, 0.95) from text("MANIC") {
// fly-in start: golden-angle scatter around the canvas centre (540, 960)
let din = 500 + mod(i * 97, 520);
let sx = 540 + cos(i * 2.39996) * din;
let sy = 960 + sin(i * 2.39996) * din;
// burst-out end: a different hashed angle
let ex = 540 + cos(i * 4.123) * (520 + mod(i * 53, 420));
let ey = 960 + sin(i * 4.123) * (520 + mod(i * 53, 420));
// timeline: assemble ~t=1.4 (per-dot stagger), hold, burst ~t=7.5
let p = t - mod(i * 7, 29) * 0.05;
let asm = 0.5 * (1 + tanh((p - 1.4) * 2.0));
let bst = 0.5 * (1 + tanh((t - 7.5) * 1.6));
let x = sx * (1 - asm) + (hx * (1 - bst) + ex * bst) * asm;
let y = sy * (1 - asm) + (hy * (1 - bst) + ey * bst) * asm;
let r = 2.4;
// a left-to-right rainbow across the word, gently cycling
let hue = mod(hx * 0.5 + t * 25, 360);
}
wait(11);
cloud-lissajous
The SAME cloud primitive as popkorn, a completely different picture: a Lissajous curve of 600
points whose phase drifts with t so the figure folds through itself. Proof the primitive is generic.
// cloud-lissajous — the SAME `cloud` primitive as popkorn, a completely
// different picture: a Lissajous curve traced by 600 points, its horizontal
// phase drifting with time so the figure slowly folds through itself. Pure math,
// no donut in sight — proof that `cloud` is a generic point field. On a 9:16
// Short, held 20s so you can watch it evolve.
//
// manic examples/cloud-lissajous.manic
title("cloud — a breathing Lissajous figure");
canvas("9:16");
template("black");
cloud(liss, 600, #46e2c8, 0.95) {
let u = i / 600 * tau; // parameter around the curve
let x = 540 + 460 * sin(3 * u + t);
let y = 960 + 760 * sin(2 * u);
let r = 3;
// colour reveal: starts near cyan (hue 180), fans into a rotating rainbow
// around the curve as `spread` grows 0 -> 1 over the first several seconds
let spread = tanh(t * 0.3);
let hue = mod(180 + u * 57 * spread + t * 35, 360);
}
wait(25);
cloud-starfield
cloud again as a scatter, not a curve: a drifting starfield built on noise and mod — hashed
positions raining downward and wrapping, with hashed sizes.
// cloud-starfield — again the same primitive, now a scatter, not a curve: a
// drifting starfield built on `noise` and `mod`. Each star sits at a hashed
// position and rains downward, wrapping at the bottom, with a hashed size — the
// point being that `cloud` places points by whatever rule you give it. A 9:16
// Short held 20s so the drift is visible.
//
// manic examples/cloud-starfield.manic
title("cloud — a drifting starfield");
canvas("9:16");
template("black");
cloud(stars, 900, #ffffff, 0.9) {
let x = 1080 * noise(i, 1);
// fall over time and wrap the height with mod(...) (a 2-arg formula function)
let y = mod(1920 * noise(i, 7) + t * 90, 1920);
let r = 0.6 + 1.8 * noise(i, 3);
// colour reveal: stars start cool blue-white, then drift into gentle
// per-star colour as `spread` grows and the whole field slowly cycles
let spread = tanh(t * 0.25);
let hue = mod(210 + 150 * noise(i, 5) * spread + t * 18, 360);
}
wait(20);
cloud-wave-lattice
cloud as a physics-flavoured field: a 50x50 lattice unpacked from the 1-D index with floor/mod,
rippling as a travelling wave driven by t.
// cloud-wave-lattice — a 50x50 grid of points unpacked from the 1-D index with
// floor/mod, rippling as a travelling wave. Same `cloud` primitive, a physics-
// flavoured field this time: index arithmetic gives you a lattice, and `t` drives
// the wave. On a 9:16 Short, held 20s so the ripple travels through fully.
//
// manic examples/cloud-wave-lattice.manic
title("cloud — a rippling 50x50 lattice");
canvas("9:16");
template("black");
cloud(sheet, 2500, #7cf05a, 0.95) {
let col = mod(i, 50);
let row = floor(i / 50);
let x = 60 + col * 19.5;
let y = 220 + row * 28 + 70 * sin(col * 0.4 + row * 0.2 + t * 2);
let r = 3;
// colour reveal: starts near green (hue 140), then fans into rainbow bands
// running down/across the sheet as `spread` grows and the wheel cycles
let spread = tanh(t * 0.3);
let hue = mod(140 + (row * 7 + col * 3) * spread + t * 30, 360);
}
wait(20);
string-art-breath
190 straight chords (point i to point 2i) whose envelope is a cardioid caustic — an ‘eye’. It
blooms from nothing, breathes to a crimson climax, then dissolves: a differential link+turn show.
// string-art-breath — a breathing string-art caustic (the "eye"), with an arc.
//
// 190 straight chords join point i to point 2i; their envelope is a cardioid
// caustic — an "eye" — and `link` keeps each string on its two anchors. Winding an
// anchor by an amount proportional to its index (a differential turn, not a rigid
// spin) grows and shrinks the caustic. The show: the eye BLOOMS from nothing,
// BREATHES with a quickening tempo up to a CLIMAX (flushing crimson), settles, then
// DISSOLVES back to nothing — every string dead straight the whole time.
//
// manic examples/string-art-breath.manic
canvas(1000, 1000);
template("paper");
let n = 190;
let cx = 500;
let cy = 500;
let rr = 430;
let pi = 3.14159265;
for i in 0..n {
let a = i * 2 * pi / n;
dot(o{i}, (cx + rr * cos(a), cy + rr * sin(a)), 1); hidden(o{i});
dot(p{i}, (cx + rr * cos(a), cy + rr * sin(a)), 1); hidden(p{i}); // starts unwound -> blank
link(s{i}, o{i}, p{i});
color(s{i}, #101010);
stroke(s{i}, 0.5);
tag(s{i}, strings);
}
// the wordmark, waiting in the wings for the finale
text(word, (cx, cy), "manic"); size(word, 104); color(word, #101010); hidden(word);
par {
// the breath: bloom -> breathe (quickening) -> climax -> settle -> dissolve.
// every turn is on one anchor by an index-proportional amount, so the caustic
// grows and shrinks as one.
for i in 0..n {
let f = i * 360 / n; // full wind: takes point i to point 2i (the eye forms)
let d = f * 0.45; // breath depth
seq {
turn(p{i}, (cx, cy), f, 4.0, out); // BLOOM into the eye
turn(p{i}, (cx, cy), -d, 3.0, smooth); // breathe open
turn(p{i}, (cx, cy), d, 3.0, smooth); // close
turn(p{i}, (cx, cy), -d * 1.2, 2.0, smooth); // deeper, quicker
turn(p{i}, (cx, cy), d * 1.2, 2.0, smooth);
turn(p{i}, (cx, cy), -d * 1.45, 1.4, smooth); // CLIMAX
turn(p{i}, (cx, cy), d * 1.45, 1.4, smooth);
turn(p{i}, (cx, cy), -d, 3.2, smooth); // settle
turn(p{i}, (cx, cy), d, 3.2, smooth);
turn(p{i}, (cx, cy), -f, 4.5, in); // DISSOLVE back to nothing
}
}
// colour drama: an ink eye that flushes crimson through the climax, then cools,
// and finally fades right out so no wire is left for the finale.
seq {
wait(9.0);
recolor(strings, #a80028, 3.0); // blood rushes in as it quickens
recolor(strings, #101010, 6.0); // cools back to ink
wait(6.5);
fade(strings, 3.0); // every string gone by ~27.5s
}
// finale: once the wires are gone, the wordmark rises
seq {
wait(27.5);
show(word, 1.4);
recolor(word, #ff2d95, 0.8); // a brand-magenta beat
pulse(word);
wait(1.4);
}
}
wheel-radial
Sixty hollow rings breathe in a travelling wave while the whole wheel spins steady->fast->slow and the
wordmark cycles the palette — one par composing breathe + eased turn + recolor; a #hex hollow-fill trick.
// wheel-radial — a breathing radial burst that also spins with a tempo arc.
//
// 60 fixed spokes tipped with HOLLOW rings. Two things happen at once, in a
// `par { }` block:
// 1. every ring BREATHES (radius oscillates) with a phase = its position, so
// the size-wave travels around the ring (the reference-clip illusion);
// 2. the whole wheel TURNS about its centre with a steady -> fast -> slow
// tempo (three eased `turn`s), and the wordmark cycles through the palette.
// The breathing period is fixed, so the *speed* change comes from the rotation —
// `breathe` drives scale, `turn` drives position, so they compose cleanly.
//
// Rings are made truly hollow by filling them with the exact paper colour.
//
// manic examples/wheel-radial.manic
canvas(1080, 1080);
template("paper");
let n = 60;
let cx = 540;
let cy = 540;
let rin = 250; // inner radius
let len = 190; // spoke length -> outer radius = rin + len
let pi = 3.14159265;
let lobes = 5; // how many fat arcs travel around at once
// 1) the frame: spokes + hollow rings, all tagged `wheel` so `turn` spins them
for i in 0..n {
let ang = i * 2 * pi / n;
let ix = cx + rin * cos(ang);
let iy = cy + rin * sin(ang);
let ox = cx + (rin + len) * cos(ang);
let oy = cy + (rin + len) * sin(ang);
line(spoke{i}, (ix, iy), (ox, oy));
stroke(spoke{i}, 1.2);
color(spoke{i}, #3a3a4a);
tag(spoke{i}, wheel);
tag(spoke{i}, spokes);
circle(cin{i}, (ix, iy), 5);
stroke(cin{i}, 2);
color(cin{i}, #f5f2e5); // hollow: fill matches the paper background
tag(cin{i}, wheel);
circle(cout{i}, (ox, oy), 12);
stroke(cout{i}, 2.5);
color(cout{i}, #f5f2e5);
tag(cout{i}, wheel);
}
// the centre hub stays still, with the wordmark inside it
circle(hub, (cx, cy), 150);
stroke(hub, 2.5);
color(hub, #f5f2e5);
text(word, (cx, cy), "manic");
size(word, 66);
color(word, #ff2d95);
par {
// rings breathe at once, phase = position -> the wave travels
for i in 0..n {
let ph = lobes * i / n;
breathe(cin{i}, 2.4, 0.85, ph, 18);
breathe(cout{i}, 2.4, 0.92, ph, 18);
}
// the wheel spins: steady, then fast, then easing to a slow stop
seq {
turn(wheel, (cx, cy), 100, 6, linear); // steady
turn(wheel, (cx, cy), 320, 4, in); // accelerate -> fast
turn(wheel, (cx, cy), 150, 8, out); // decelerate -> slow stop
}
// and the wordmark cycles through the palette on the way
seq {
wait(6); recolor(word, #00e6ff, 1.2); recolor(spokes, #00e6ff, 1.6);
wait(4); recolor(word, #7cff6b, 1.2);
wait(3); recolor(word, #ffd166, 1.2);
}
}
wheel-square
The square sibling of wheel-radial on black — hollow SQUARES on a square. It starts DEAD STILL (the travelling breath-wave fakes rotation), then after ~6s really spins counter-clockwise; sized to stay in-frame when spun.
// wheel-square — the square sibling of wheel-radial: a breathing burst whose rings
// sit on a SQUARE, on black.
//
// The reveal: it starts DEAD STILL — only the rings breathe, and because each ring's
// phase = its position, the size-wave travels and FAKES a rotation though nothing
// moves. After ~6s the trick is dropped and the whole burst actually spins
// COUNTER-CLOCKWISE (steady -> fast -> slow), the wordmark cycling colour.
//
// Each spoke's tip is projected onto a square instead of a circle: a ray at angle
// `ang` hits a square of half-width R at distance R / max(|cos|,|sin|).
//
// manic examples/wheel-square.manic
canvas(1080, 1080);
template("mono");
let n = 64;
let cx = 540;
let cy = 540;
let rin = 195; // inner square half-width
let len = 145; // spoke length -> outer square half-width = rin + len
let pi = 3.14159265;
let lobes = 5;
// sized so the corners (at rout*sqrt(2)) stay inside the frame even when spun
// 1) the frame: spokes + hollow SQUARES on a square, tagged `wheel` so `turn` spins them
for i in 0..n {
let ang = i * 2 * pi / n;
let c = cos(ang);
let s = sin(ang);
let ca = abs(c);
let sa = abs(s);
let m = 0.5 * (ca + sa + abs(ca - sa)); // = max(|cos|,|sin|): ray -> square edge
let ix = cx + (rin / m) * c;
let iy = cy + (rin / m) * s;
let ox = cx + ((rin + len) / m) * c;
let oy = cy + ((rin + len) / m) * s;
line(spoke{i}, (ix, iy), (ox, oy));
stroke(spoke{i}, 1.2);
color(spoke{i}, #55607a);
tag(spoke{i}, wheel);
tag(spoke{i}, spokes);
rect(cin{i}, (ix, iy), 9, 9);
stroke(cin{i}, 2);
color(cin{i}, #000000); // hollow: fill matches the black background
tag(cin{i}, wheel);
rect(cout{i}, (ox, oy), 22, 22);
stroke(cout{i}, 2.5);
color(cout{i}, #000000);
tag(cout{i}, wheel);
}
// the centre hub stays still, with the wordmark inside it (a square frame too)
rect(hub, (cx, cy), 300, 300);
stroke(hub, 2.5);
color(hub, #000000);
text(word, (cx, cy), "manic");
size(word, 66);
color(word, #ff2d95);
par {
// rings breathe the WHOLE time, phase = position -> the wave travels
for i in 0..n {
let ph = lobes * i / n;
breathe(cin{i}, 2.4, 0.85, ph, 24);
breathe(cout{i}, 2.4, 0.92, ph, 24);
}
// hold still for the illusion, THEN spin COUNTER-CLOCKWISE: steady -> fast -> slow
seq {
wait(6); // just the breathing fakes rotation
turn(wheel, (cx, cy), -100, 6, linear); // now it really turns: steady
turn(wheel, (cx, cy), -320, 4, in); // accelerate -> fast
turn(wheel, (cx, cy), -150, 8, out); // decelerate -> slow stop
}
// the wordmark cycles through the palette once the spin begins
seq {
wait(6); recolor(word, #00e6ff, 1.2); recolor(spokes, #00e6ff, 1.6);
wait(5); recolor(word, #7cff6b, 1.2);
wait(4); recolor(word, #ffd166, 1.2);
}
}
wheel-duo
Four breathing bursts in a 2x2 on black with HUE’d rainbow spokes, spinning forever: circle & square
rigid (top), and circle & square counter-spinning (bottom) — the rainbow links twist into a spirograph eye.
// wheel-duo — four breathing bursts in a 2x2 grid on black, spinning forever.
//
// top row : normal spin (inner+outer together) — circle | square
// bottom row : COUNTER-spin (outer clockwise, inner counter-clockwise) — circle | square
//
// The spokes are `link`s that follow their two dots, so counter-rotation twists them
// into a spirograph "eye". Each spoke is HUE'd by its angle -> a rainbow wheel; the
// continuous rotation carries the rainbow around and never stops. Every burst
// breathes, holds still (the breath-wave fakes rotation), then keeps spinning.
//
// manic examples/wheel-duo.manic
canvas(1600, 1600);
template("black");
let n = 44;
let rin = 125;
let len = 85; // outer = rin + len = 210
let pi = 3.14159265;
let lobes = 4;
// ---- A: circle, top-left (normal spin) ----
for i in 0..n {
let ang = i * 2 * pi / n;
let ix = 440 + rin * cos(ang); let iy = 440 + rin * sin(ang);
let ox = 440 + (rin + len) * cos(ang); let oy = 440 + (rin + len) * sin(ang);
circle(a_ci{i}, (ix, iy), 5); stroke(a_ci{i}, 1.8); color(a_ci{i}, #000000); tag(a_ci{i}, a_all);
circle(a_co{i}, (ox, oy), 10); stroke(a_co{i}, 2.2); color(a_co{i}, #000000); tag(a_co{i}, a_all);
link(a_sp{i}, a_ci{i}, a_co{i}); hue(a_sp{i}, 360 * i / n); stroke(a_sp{i}, 1.3);
}
// ---- B: square, top-right (normal spin) ----
for i in 0..n {
let ang = i * 2 * pi / n;
let c = cos(ang); let s = sin(ang);
let m = 0.5 * (abs(c) + abs(s) + abs(abs(c) - abs(s))); // max(|cos|,|sin|)
let ix = 1160 + (rin / m) * c; let iy = 440 + (rin / m) * s;
let ox = 1160 + ((rin + len) / m) * c; let oy = 440 + ((rin + len) / m) * s;
rect(b_ci{i}, (ix, iy), 9, 9); stroke(b_ci{i}, 1.8); color(b_ci{i}, #000000); tag(b_ci{i}, b_all);
rect(b_co{i}, (ox, oy), 19, 19); stroke(b_co{i}, 2.2); color(b_co{i}, #000000); tag(b_co{i}, b_all);
link(b_sp{i}, b_ci{i}, b_co{i}); hue(b_sp{i}, 360 * i / n); stroke(b_sp{i}, 1.3);
}
// ---- C: circle, bottom-left (COUNTER: outer cw, inner ccw) ----
for i in 0..n {
let ang = i * 2 * pi / n;
let ix = 440 + rin * cos(ang); let iy = 1160 + rin * sin(ang);
let ox = 440 + (rin + len) * cos(ang); let oy = 1160 + (rin + len) * sin(ang);
circle(c_ci{i}, (ix, iy), 5); stroke(c_ci{i}, 1.8); color(c_ci{i}, #000000); tag(c_ci{i}, c_in);
circle(c_co{i}, (ox, oy), 10); stroke(c_co{i}, 2.2); color(c_co{i}, #000000); tag(c_co{i}, c_out);
link(c_sp{i}, c_ci{i}, c_co{i}); hue(c_sp{i}, 360 * i / n); stroke(c_sp{i}, 1.3);
}
// ---- D: square, bottom-right (COUNTER: outer cw, inner ccw) ----
for i in 0..n {
let ang = i * 2 * pi / n;
let c = cos(ang); let s = sin(ang);
let m = 0.5 * (abs(c) + abs(s) + abs(abs(c) - abs(s)));
let ix = 1160 + (rin / m) * c; let iy = 1160 + (rin / m) * s;
let ox = 1160 + ((rin + len) / m) * c; let oy = 1160 + ((rin + len) / m) * s;
rect(d_ci{i}, (ix, iy), 9, 9); stroke(d_ci{i}, 1.8); color(d_ci{i}, #000000); tag(d_ci{i}, d_in);
rect(d_co{i}, (ox, oy), 19, 19); stroke(d_co{i}, 2.2); color(d_co{i}, #000000); tag(d_co{i}, d_out);
link(d_sp{i}, d_ci{i}, d_co{i}); hue(d_sp{i}, 360 * i / n); stroke(d_sp{i}, 1.3);
}
// labels + centre title
text(title, (800, 800), "manic"); size(title, 62); color(title, #ff2d95);
text(la, (440, 720), "circle"); size(la, 26); color(la, #7f8aa3);
text(lb, (1160, 720), "square"); size(lb, 26); color(lb, #7f8aa3);
text(lc, (440, 1500), "circle counter"); size(lc, 26); color(lc, #7f8aa3);
text(ld, (1160, 1500), "square counter"); size(ld, 26); color(ld, #7f8aa3);
par {
// all four bursts breathe the whole time
for i in 0..n {
let ph = lobes * i / n;
breathe(a_ci{i}, 2.4, 0.85, ph, 38); breathe(a_co{i}, 2.4, 0.92, ph, 38);
breathe(b_ci{i}, 2.4, 0.85, ph, 38); breathe(b_co{i}, 2.4, 0.92, ph, 38);
breathe(c_ci{i}, 2.4, 0.85, ph, 38); breathe(c_co{i}, 2.4, 0.92, ph, 38);
breathe(d_ci{i}, 2.4, 0.85, ph, 38); breathe(d_co{i}, 2.4, 0.92, ph, 38);
}
// hold still, ease in, then spin CONTINUOUSLY (never stops). top: rigid; bottom: counter.
seq { wait(5); turn(a_all, (440, 440), 90, 3, in); turn(a_all, (440, 440), 1800, 30, linear); }
seq { wait(5); turn(b_all, (1160, 440), -90, 3, in); turn(b_all, (1160, 440), -1800, 30, linear); }
seq { wait(5); turn(c_out, (440, 1160), 90, 3, in); turn(c_out, (440, 1160), 1800, 30, linear); }
seq { wait(5); turn(c_in, (440, 1160), -90, 3, in); turn(c_in, (440, 1160), -1800, 30, linear); }
seq { wait(5); turn(d_out, (1160, 1160), 90, 3, in); turn(d_out, (1160, 1160), 1800, 30, linear); }
seq { wait(5); turn(d_in, (1160, 1160), -90, 3, in); turn(d_in, (1160, 1160), -1800, 30, linear); }
}
lsystem-asymptote-curves
Four canonical Asymptote rewriting systems become fitted, continuously drawable Manic paths—including a concave filled boundary and a 9,604-segment carpet curve.
// Four classic deterministic curves from the Asymptote example corpus.
// Each figure is one fitted, traceable Manic entity—even the 9,604-segment curve.
title("Four Rules, Four Infinite-Looking Curves");
canvas("16:9");
template("mono");
watermark(mark, (w*0.105, h*0.08), "Made With Manic");
text(kicker, (cx, h*0.075), "GENERATIVE GEOMETRY · L-SYSTEMS");
text(headline, (cx, h*0.135), "A tiny rewriting rule becomes a continuous path");
text(caption, (cx, h*0.92), "One path per curve · auto-fitted · continuously drawable");
size(kicker, 20); bold(kicker); color(kicker, dim);
size(headline, 34); bold(headline);
size(caption, 20); color(caption, dim);
let left = w*0.275;
let right = w*0.725;
let upper = h*0.37;
let lower = h*0.70;
let cell = h*0.27;
lsystem(sierpinski, (left, upper), cell,
"YF", "X=YF+XF+Y;Y=XF-YF-X",
"angle=60 heading=0 iterations=7");
color(sierpinski, cyan); stroke(sierpinski, 2.5); untraced(sierpinski);
lsystem(gosper, (right, upper), cell,
"FX", "X=X+YF++YF-FX--FXFX-YF+;Y=-FX+YFYF++YF+FX--FX-Y",
"angle=60 heading=0 iterations=4");
color(gosper, magenta); stroke(gosper, 2.5); untraced(gosper);
lsystem(squareCurve, (left, lower), cell,
"F+XF+F+XF", "X=XF-F+F-XF+F+XF-F+F-X",
"angle=90 heading=45 iterations=5 closed=true fill=true");
color(squareCurve, gold); opacity(squareCurve, 0.70); stroke(squareCurve, 2.0); untraced(squareCurve);
lsystem(carpet, (right, lower), cell,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 heading=0 iterations=4");
color(carpet, lime); stroke(carpet, 2.0); untraced(carpet);
text(l1, (left, h*0.205), "SIERPINSKI CURVE · 2,187 SEGMENTS");
text(l2, (right, h*0.205), "PEANO–GOSPER · 2,401 SEGMENTS");
text(l3, (left, h*0.535), "SQUARE CURVE · 5,460 SEGMENTS");
text(l4, (right, h*0.535), "CARPET CURVE · 9,604 SEGMENTS");
size(l1, 18); size(l2, 18); size(l3, 18); size(l4, 18);
bold(l1); bold(l2); bold(l3); bold(l4);
color(l1, cyan); color(l2, magenta); color(l3, gold); color(l4, lime);
hidden(l1); hidden(l2); hidden(l3); hidden(l4);
step("one rule becomes a curve") {
par {
show(l1, 0.35);
draw(sierpinski, 1.8, smooth);
}
}
wait(0.30);
step("change the grammar") {
par {
show(l2, 0.35);
draw(gosper, 1.8, smooth);
}
}
wait(0.30);
step("close and fill the boundary") {
par {
show(l3, 0.35);
draw(squareCurve, 1.8, smooth);
}
}
wait(0.30);
step("thousands of segments stay one path") {
par {
show(l4, 0.35);
draw(carpet, 2.2, smooth);
}
}
wait(1.20);
creator-lsystem-fractal-curve
A creator Short follows one seven-segment rule from a four-edge square to a 9,604-segment space-filling curve, then closes with the Manic CTA.
// Creator story: one seven-segment rewriting rule grows from a square into a
// 9,604-segment space-filling curve. The rule is the story—not implementation.
title("How One Line Learns to Fill Space");
canvas("9:16");
template("mono");
creator(me, "@anish2good name=Manic_Geometry tagline=Rules_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Animate_your_idea safe=clean");
socials(me);
watermark(manicMark, (w*0.15, h*0.06), "Made With Manic");
endcard(me, "title=Turn_Rules_Into_Stories cta=8gwifi.org/manic");
let u = (w+h-abs(w-h))/1080;
text(kicker, (cx, h*0.14), "MANIC · GENERATIVE GEOMETRY");
text(headline, (cx, h*0.24), "One rule. 9,604 lines.");
text(caption, (cx, h*0.79), "Start with a square.");
text(generation, (cx, h*0.69), "GENERATION 0 · 4 SEGMENTS");
text(rule, (cx, h*0.30), "F → FF + F + F + F + FF");
size(kicker, 20*u); bold(kicker); color(kicker, cyan);
size(headline, 30*u); bold(headline); wrap(headline, w*0.78);
size(caption, 23*u); bold(caption); wrap(caption, w*0.74);
size(generation, 20*u); bold(generation); color(generation, dim);
size(rule, 25*u); bold(rule); color(rule, gold);
let stageSize = (w+h-abs(w-h))*0.28;
let stageY = h*0.49;
lsystem(curve, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=0");
color(curve, cyan); stroke(curve, 5);
lsystem(gen1, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=1");
color(gen1, cyan); stroke(gen1, 4); hidden(gen1);
lsystem(gen2, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=2");
color(gen2, cyan); stroke(gen2, 3.5); hidden(gen2);
lsystem(gen3, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=3");
color(gen3, magenta); stroke(gen3, 3); hidden(gen3);
lsystem(finalCurve, (cx, stageY), stageSize,
"F+F+F+F", "F=FF+F+F+F+FF",
"angle=90 iterations=4");
gradient(finalCurve, cyan, magenta, gold);
stroke(finalCurve, 2.2); untraced(finalCurve); hidden(finalCurve);
hidden(kicker); hidden(headline); hidden(rule);
step("ask the impossible question") {
seq {
par {
show(kicker, 0.35);
show(headline, 0.50);
show(rule, 0.50);
show(curve, 0.40);
}
pulse(curve, 0.60);
}
}
wait(0.45);
step("rewrite every forward move") {
seq {
say(caption, "Replace every F with seven smaller forward moves.", 0.45, smooth);
par {
become(curve, gen1, 0.85, smooth);
say(generation, "GENERATION 1 · 28 SEGMENTS", 0.35, smooth);
}
par {
become(curve, gen2, 0.95, smooth);
say(generation, "GENERATION 2 · 196 SEGMENTS", 0.35, smooth);
}
par {
become(curve, gen3, 1.05, smooth);
say(generation, "GENERATION 3 · 1,372 SEGMENTS", 0.35, smooth);
}
}
}
wait(0.50);
step("let the path fill space") {
seq {
par {
fade(curve, 0.35);
say(caption, "Repeat once more. The same rule now draws 9,604 connected segments.", 0.45, smooth);
say(generation, "GENERATION 4 · 9,604 SEGMENTS", 0.35, smooth);
}
show(finalCurve, 0.05);
draw(finalCurve, 3.20, smooth);
pulse(finalCurve, 0.80);
}
}
wait(0.75);
step("the idea is the animation") {
seq {
say(caption, "In Manic, creators describe the rule. The engine makes it move.", 0.45, smooth);
par {
recolor(headline, gold, 0.50);
pulse(finalCurve, 0.85);
}
}
}
wait(1.00);
step("creator call to action") {
par {
fade(kicker, 0.35); fade(headline, 0.35); fade(rule, 0.35);
fade(caption, 0.35); fade(generation, 0.35); fade(finalCurve, 0.45);
fade(me.footer, 0.35);
show(me.endcard, 0.60);
}
}
wait(1.80);
asymptote-tiling-reference
One two-dimensional motif becomes hex rings, a rotated grid, an outward-facing radial
system, and a nested motif-of-motifs—all through the generic repeat foundation.
// The recurring structure behind Asymptote's tiling examples:
// author one motif, then arrange it as a hex field, grid, radial ring, or a
// repeated composition. Every generated tile remains a normal Manic entity.
title("One Motif, Four Tiling Systems");
canvas("16:9");
template("mono");
watermark(mark, (w*0.11, h*0.075), "Made With Manic");
text(kicker, (cx, h*0.07), "GENERATIVE GEOMETRY · REPEAT");
text(headline, (cx, h*0.13), "Build the motif once. Compose the field.");
size(kicker, 19); bold(kicker); color(kicker, dim);
size(headline, 34); bold(headline);
let lx = w*0.27;
let rx = w*0.73;
let uy = h*0.37;
let ly = h*0.73;
// Hex rings: a small two-part diamond becomes a honeycomb field.
polygon(hexBody, (lx,uy-16), (lx+15,uy), (lx,uy+16), (lx-15,uy));
circle(hexCore, (lx,uy), 4);
color(hexBody, cyan); color(hexCore, gold);
tag(hexBody, hexMotif); tag(hexCore, hexMotif);
repeat(hexField, hexMotif, "layout=hex rings=4 spacing=30 rotate=30 scale=0.82");
hidden(hexMotif); untraced(hexField);
// Grid: a deliberately asymmetric motif proves orientation is retained.
line(gridStem, (rx-15,uy+12), (rx+14,uy-12));
circle(gridTip, (rx+14,uy-12), 5);
color(gridStem, magenta); color(gridTip, lime);
stroke(gridStem, 3);
tag(gridStem, gridMotif); tag(gridTip, gridMotif);
repeat(gridField, gridMotif, "layout=grid rows=5 cols=7 gapx=48 gapy=42 rotate=-8");
hidden(gridMotif); untraced(gridField);
// Radial: each arrow-shaped wedge faces away from the common centre.
polygon(ray, (lx,ly-22), (lx+9,ly-5), (lx,ly+4), (lx-9,ly-5));
color(ray, gold); tag(ray, rayMotif);
repeat(sun, rayMotif, "layout=radial count=18 radius=112 face=out rotate=10 scale=0.85");
hidden(rayMotif); untraced(sun);
// Nested composition: repeat a 2x2 micro-pattern as one larger radial motif.
polygon(seed, (rx-7,ly+7), (rx+7,ly+7), (rx,ly-8));
color(seed, cyan);
repeat(micro, seed, "layout=grid rows=2 cols=2 gapx=20 gapy=20 scale=0.70");
repeat(nested, micro, "layout=radial count=10 radius=105 face=out rotate=18 scale=0.82");
hidden(seed); hidden(micro); untraced(nested);
text(l1, (lx,h*0.205), "HEX RINGS · 37 MOTIFS");
text(l2, (rx,h*0.205), "ROTATED GRID · 35 MOTIFS");
text(l3, (lx,h*0.565), "RADIAL · FACE OUT");
text(l4, (rx,h*0.565), "NESTED · MOTIFS OF MOTIFS");
size(l1,17); size(l2,17); size(l3,17); size(l4,17);
bold(l1); bold(l2); bold(l3); bold(l4);
color(l1,cyan); color(l2,magenta); color(l3,gold); color(l4,lime);
step("hexagonal rings") { draw(hexField, 1.40, smooth); }
wait(0.25);
step("rectangular repetition") { draw(gridField, 1.40, smooth); }
wait(0.25);
step("radial orientation") { draw(sun, 1.20, smooth); }
wait(0.25);
step("composition remains reusable") { draw(nested, 1.60, smooth); }
wait(1.30);
creator-one-tile-pattern-story
A creator problem asks how many tiles lie in three complete hexagonal rings. Stable repeat layers, a live total, semantic colour, and LaTeX derive 1+6+12+18 = 37.
// Creator problem: count a hexagonal mosaic without counting 37 tiles one by
// one. `repeat` constructs the exact layers; counters and semantic LaTeX turn
// the geometry into a short visual proof.
title("How Many Tiles Are in Three Hexagonal Rings?");
canvas("9:16");
template("blank");
creator(me, "@anish2good name=Manic_Geometry tagline=Patterns_made_visible yt=zarigatongy x=@anish2good web=8gwifi.org/manic accent=cyan secondary=magenta footer=compact cta=Create_without_keyframes safe=clean");
socials(me);
watermark(mark, (w*0.16, h*0.055), "Made With Manic");
endcard(me, "title=Turn_Patterns_Into_Proofs cta=8gwifi.org/manic");
let u = (w+h-abs(w-h))/1080;
let boardY = h*0.43;
let tileR = 19*u;
text(kicker, (cx,h*0.105), "MANIC · VISUAL COUNTING");
text(headline, (cx,h*0.17), "Three rings surround one tile.");
text(question, (cx,h*0.235), "How many tiles are there altogether?");
text(caption, (cx,h*0.77), "Do not count one by one. Count what each ring adds.");
size(kicker, 20*u); bold(kicker); color(kicker, cyan);
size(headline, 31*u); bold(headline); wrap(headline,w*0.80);
size(question, 25*u); bold(question); wrap(question,w*0.78); color(question,gold);
size(caption, 22*u); bold(caption); wrap(caption,w*0.78); color(caption,dim);
// One regular hexagonal tile is the only authored artwork.
polygon(tile,
(cx,boardY-tileR),
(cx+0.866*tileR,boardY-0.5*tileR),
(cx+0.866*tileR,boardY+0.5*tileR),
(cx,boardY+tileR),
(cx-0.866*tileR,boardY+0.5*tileR),
(cx-0.866*tileR,boardY-0.5*tileR));
color(tile, gold); glow(tile, 0.75);
// The question silhouette: all 37 tiles, deliberately subdued.
repeat(questionField, tile,
"layout=hex rings=4 spacing=39 rotate=30 scale=0.90");
color(questionField, dim); opacity(questionField,0.30);
untraced(questionField);
// Declare largest first and smallest last. When all four groups are visible,
// the later cumulative layers cover their shared interior, leaving each newly
// added ring in its own semantic colour.
repeat(layer4, tile,
"layout=hex rings=4 spacing=39 rotate=30 scale=0.90");
color(layer4,lime); hidden(layer4); untraced(layer4);
repeat(layer3, tile,
"layout=hex rings=3 spacing=39 rotate=30 scale=0.90");
color(layer3,magenta); hidden(layer3); untraced(layer3);
repeat(layer2, tile,
"layout=hex rings=2 spacing=39 rotate=30 scale=0.90");
color(layer2,cyan); hidden(layer2); untraced(layer2);
repeat(layer1, tile,
"layout=hex rings=1 spacing=39 rotate=30 scale=0.90");
color(layer1,gold); hidden(layer1); untraced(layer1);
hidden(tile);
counter(total, (cx,h*0.655), 1, 0, "TOTAL ", " TILES");
size(total,25*u); bold(total); color(total,gold); hidden(total);
equation(work, (cx,h*0.70),
`N=\textcolor{gold}{1}+\textcolor{cyan}{6}+\textcolor{magenta}{12}+\textcolor{lime}{18}`,
31*u);
hidden(work);
hidden(kicker); hidden(headline); hidden(question); hidden(caption);
step("pose the mosaic problem") {
par {
show(kicker,0.35);
show(headline,0.50);
show(question,0.45);
show(caption,0.40);
draw(questionField,1.25,smooth);
}
}
wait(1.10);
step("focus on the construction") {
par {
fade(questionField,0.35);
say(caption,"Begin with the single centre tile.",0.40);
cam((cx,boardY),0.45,smooth);
zoom(1.10,0.45,smooth);
show(total,0.35);
}
show(layer1,0.05);
draw(layer1,0.45,smooth);
pulse(layer1,0.55);
}
wait(0.40);
step("the first ring adds six") {
par {
show(layer2,0.05);
to(total,value,7,0.55,smooth);
say(caption,"Ring 1 adds 6 tiles: one on each side.",0.40);
}
stagger(0.055) {
for i in 0..7 { draw(layer2.i{i},0.28,smooth); }
}
}
wait(0.35);
step("the second ring adds twelve") {
par {
show(layer3,0.05);
to(total,value,19,0.65,smooth);
say(caption,"Ring 2 has twice as many positions, so it adds 12.",0.45);
}
stagger(0.035) {
for i in 0..19 { draw(layer3.i{i},0.22,smooth); }
}
}
wait(0.35);
step("the third ring adds eighteen") {
par {
show(layer4,0.05);
to(total,value,37,0.75,smooth);
say(caption,"Ring 3 adds 18 more. Every new ring contributes another six.",0.45);
}
stagger(0.022) {
for i in 0..37 { draw(layer4.i{i},0.18,smooth); }
}
}
wait(0.55);
step("write what the colors counted") {
par {
show(work,0.50);
say(caption,"The colored layers give the sum directly.",0.40);
cam((cx,cy),0.45,smooth);
zoom(1.0,0.45,smooth);
}
}
wait(0.55);
step("recognize the pattern") {
rewrite(work, `N=1+6(1+2+3)`,0.80,smooth);
say(caption,"Factor out six: the ring numbers form a triangular sum.",0.45);
}
wait(0.55);
step("solve") {
par {
rewrite(work, `N=\textcolor{lime}{37}`,0.85,smooth);
say(caption,"So the mosaic contains exactly 37 tiles.",0.45);
pulse(total,0.80);
recolor(headline,gold,0.50);
}
}
wait(1.10);
step("call to action") {
par {
fade(kicker,0.30); fade(headline,0.30); fade(question,0.30);
fade(caption,0.30); fade(total,0.30); fade(work,0.30);
fade(layer1,0.35); fade(layer2,0.35); fade(layer3,0.35); fade(layer4,0.35);
fade(me.footer,0.30); show(me.endcard,0.60);
}
}
wait(1.80);
gun-shot
A pure-imagination SCENE — no physics kit, just storytelling: a gun fires, the camera
flies along with the bullet (cam/zoom), a block drops in out of nowhere, and BOOM —
flash/shake/pulse + a for-loop spark burst. manic as a movie language.
// ============================================================================
// gun-shot.manic — a scene, not a lesson. No physics kit, just imagination.
// ----------------------------------------------------------------------------
// A gun fires · the camera races along with the bullet · a block drops in out
// of nowhere · BOOM. Built entirely from base manic — shapes, `move`, `cam`/
// `zoom` to fly the camera, `flash`/`shake`/`pulse`, and a `for`-loop spark
// burst. This is manic as a storytelling language: dream a scene, write it.
// ============================================================================
title("Gun Shot");
canvas("16:9");
// ---- the world (wide — the camera pans across it) ----
line(ground, (-300, 560), (2400, 560)); color(ground, dim); stroke(ground, 4);
// the gun: barrel + body + grip
rect(barrel, (250, 470), 96, 22); color(barrel, dim); filled(barrel);
rect(body, (206, 478), 52, 42); color(body, dim); filled(body);
polygon(grip, (186, 500), (220, 500), (212, 554), (180, 550), dim);
// the bullet at the muzzle, and a muzzle flash — both waiting
circle(bullet, (302, 470), 12); color(bullet, gold); glow(bullet, 2.2); hidden(bullet);
circle(mflash, (312, 470), 30); color(mflash, gold); glow(mflash, 3.5); hidden(mflash);
// the block — waiting above, off-screen, to drop in ahead
rect(block, (1750, 250), 130, 130); color(block, cyan); filled(block); glow(block, 1.4); hidden(block);
text(boom, (1750, 320), "BOOM!"); size(boom, 96); color(boom, magenta); bold(boom); glow(boom, 2.5); display(boom); hidden(boom);
// a ring of impact sparks around the block (revealed at the hit)
for i in 0..14 {
let ang = i * tau / 14.0;
line(spark{i}, (1700, 470), (1700 + 160*cos(ang), 470 + 160*sin(ang)));
color(spark{i}, gold); stroke(spark{i}, 5); glow(spark{i}, 2); untraced(spark{i}); tag(spark{i}, sparks);
}
// a caption pinned to the screen (rides along through the camera move)
text(cap, (cx, h - 56), ""); color(cap, fg); size(cap, 26); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
cam((440, 380), 0.4, smooth); // frame the gun
say(cap, "steady…", 0.4);
wait(0.6);
// FIRE!
say(cap, "FIRE!", 0.2);
par { show(mflash, 0.06); pulse(mflash); show(bullet, 0.08); }
fade(mflash, 0.3);
// the bullet races off — the camera flies with it — and mid-flight, out of
// nowhere, a block slams down into its path
par {
move(bullet, (1690, 470), 2.6, smooth);
cam((1560, 380), 2.6, smooth);
zoom(1.15, 2.6, smooth);
seq {
wait(1.5);
say(cap, "…wait — what's THAT?!", 0.3);
show(block, 0.1);
move(block, (1750, 470), 0.4, bounce);
}
}
// BOOM — impact
say(cap, "BOOM!", 0.15);
par {
flash(block, gold);
shake(block, 0.5);
zoom(1.5, 0.15);
show(boom, 0.12); pulse(boom);
draw(sparks, 0.35);
}
wait(0.5);
// settle — pull back
par {
fade(sparks, 0.5);
fade(boom, 0.6);
fade(bullet, 0.4);
zoom(1.0, 0.9, smooth);
}
say(cap, "…scene.", 0.4);
wait(0.8);
fractal_tree
One recursive def, drawn to depth 12.
// Fractal Tree — a recursive `def` macro draws a branching tree. Each branch
// splits into two shorter branches at a fixed angle; `if depth > 0` is the base
// case that stops the recursion. Branches are keyed by a binary-heap index
// (k -> 2k, 2k+1) so every segment gets a unique id, hued and thinned by depth.
//
// Showcases the Phase-2 language layer: `def`, recursion, `if`, comparisons.
//
// manic examples/fractal_tree.manic
// manic examples/fractal_tree.manic --record out --fps 60
title("Fractal Tree");
canvas(1280, 720);
text(head, (640, 92), "one recursive rule, drawn to depth 9");
display(head); color(head, cyan); size(head, 26); hidden(head);
// draw a branch, then recurse into two children (unless we've bottomed out)
def branch(k, x, y, ang, len, depth) {
// stop at the base depth OR once a branch is too short to see — so even a
// large `depth` self-limits (the tree is bounded by branch length)
if depth > 0 && len > 2 {
let x2 = x + len * cos(ang);
let y2 = y - len * sin(ang); // screen y grows downward
line(seg{k}, (x, y), (x2, y2));
stroke(seg{k}, 1 + depth * 0.8);
hue(seg{k}, 120 + depth * 15); // trunk bluish -> tips green
untraced(seg{k}); tag(seg{k}, tree);
branch(2*k, x2, y2, ang + 0.42, len * 0.72, depth - 1);
branch(2*k + 1, x2, y2, ang - 0.42, len * 0.72, depth - 1);
}
}
// grow from the bottom centre, pointing up (angle pi/2)
branch(1, 640, 700, 1.5708, 150, 20);
// --- script ---
show(head, 0.5);
draw(tree, 1.8);
wait(1.6);
particles-flow
Contained ambient motion and live curved connections in four generic words: particles,
wander, link, and flow. The ids supply the domain meaning.
// Generic contained motion: the ids give the dots their meaning.
// The same four words work for bubbles, dust, stars, data, or molecules.
title("Three bodies, one relation");
canvas("9:16");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
// No template call: black is the full-colour exact-black default.
circle(A, (540, 390), 105);
circle(B, (260, 760), 105);
circle(C, (820, 760), 105);
stroke(A, 5); stroke(B, 5); stroke(C, 5);
particles(insideA, A, 24, 5, 7);
particles(insideB, B, 24, 5, 17);
particles(insideC, C, 24, 5, 27);
equation(labelA, (540, 390), `A`, 64);
equation(labelB, (260, 760), `B`, 64);
equation(labelC, (820, 760), `C`, 64);
link(ab, A, B, -48);
link(bc, B, C, -56);
link(ac, A, C, 48);
stroke(ab, 5); stroke(bc, 5); stroke(ac, 5);
untraced(ab); untraced(bc); untraced(ac);
equation(relAB, (310, 445), `A\sim B`, 34);
equation(relBC, (540, 690), `B\sim C`, 34);
equation(relAC, (770, 445), `A\sim C`, 34);
color(relAB, dim); color(relBC, dim); color(relAC, dim);
hidden(A); hidden(B); hidden(C);
hidden(labelA); hidden(labelB); hidden(labelC);
hidden(insideA); hidden(insideB); hidden(insideC);
hidden(relAB); hidden(relBC); hidden(relAC);
par {
wander(insideA, 9);
wander(insideB, 9);
wander(insideC, 9);
seq {
par { show(A, 0.35); show(labelA, 0.35); show(insideA, 0.45); }
wait(0.25);
par { show(B, 0.35); show(labelB, 0.35); show(insideB, 0.45); }
show(relAB, 0.25);
par { draw(ab, 0.75); recolor(relAB, fg, 0.75); }
flow(ab, 0.9);
par { show(C, 0.35); show(labelC, 0.35); show(insideC, 0.45); }
show(relBC, 0.25);
par { draw(bc, 0.75); recolor(relBC, fg, 0.75); }
flow(bc, 0.9);
show(relAC, 0.25);
par { draw(ac, 0.75); recolor(relAC, fg, 0.75); }
par { flow(ab, 1.1); flow(bc, 1.1); flow(ac, 1.1); }
wait(0.55);
}
}
process-stream-observe
One deterministic collection journey drives two truthful views. stream progressively
moves persistent objects; observe connects the same arrival/speed measurements to a
counter and an initially empty livehistogram without callbacks or guessed keyframes.
// PROCESS FOUNDATION — the smallest complete example.
// A real persistent collection streams along a path. Both observers read the
// compiled process measurements; neither is animated with guessed values.
title("A Collection Becomes a Process");
canvas("16:9");
template("blank");
watermark(mark, (170, 58), "Made With Manic");
text(kicker, (640, 52), "MANIC · DETERMINISTIC PROCESS");
text(headline, (640, 100), "One journey. Two truthful views.");
size(kicker, 18); color(kicker, dim); bold(kicker);
size(headline, 34); bold(headline);
rect(source, (170, 300), 210, 230);
outlined(source); outline(source, dim); stroke(source, 3);
text(sourceLabel, (170, 440), "persistent collection");
size(sourceLabel, 18); color(sourceLabel, dim);
particles(packets, source, 42, 5, 17);
spline(route, (275, 300), (410, 140), (560, 470), (720, 285));
stroke(route, 4); color(route, fg); untraced(route);
livehistogram(speeds, (980, 335), 0.55, 1.05, 10, 430, 220, cyan);
text(speedLabel, (980, 190), "normalized speed");
size(speedLabel, 20); bold(speedLabel);
counter(arrivals, (640, 610), 0, 0, "arrived ", " / 42");
size(arrivals, 25); color(arrivals, dim);
text(caption, (640, 665), "stream moves real objects · observe reads the same process");
size(caption, 20); color(caption, dim);
hidden(packets); hidden(speeds); hidden(arrivals); hidden(caption);
step("introduce") {
par {
show(packets, 0.45);
draw(route, 0.65);
show(speeds, 0.45);
show(arrivals, 0.35);
show(caption, 0.35);
}
}
wait(0.35);
step("stream-and-observe") {
par {
stream(packets, route, 4.2, 34, smooth);
observe(speeds, packets, speed);
observe(arrivals, packets, arrived);
}
}
wait(1.0);
process-branching-dispatch
One source dispatches persistent requests through an authored one-of-three path network.
The destination histogram reads each request’s real seeded outcome; no service semantics
or separately timed chart animation are hidden in the engine.
// GENERIC PROCESS BRANCHING — one source, three destinations.
//
// The paths carry no service semantics. `branch` only sees a directed acyclic
// network and makes one deterministic uniform choice at every fork. The same
// foundation drives the Galton-board example.
title("Process Branching — One Source, Three Destinations");
canvas("16:9");
template("blank");
watermark(mark, (145, 70), "Made With Manic");
text(head, (cx, 72), "One collection · many truthful routes");
text(sub, (cx, 112),
"Every request keeps its identity, destination, step count, and arrival time.");
size(head, 32); bold(head);
size(sub, 19); color(sub, dim);
circle(source, (180, 340), 24);
color(source, panel); outline(source, cyan); stroke(source, 3);
particles(requests, source, 54, 4, 41);
color(requests, cyan); glow(requests, 0.7); z(requests, 8);
line(entry, (205, 340), (420, 340)); tag(entry, dispatchRoutes);
spline(upper, (420, 340), (530, 190), (680, 185)); tag(upper, dispatchRoutes);
line(middle, (420, 340), (680, 340)); tag(middle, dispatchRoutes);
spline(lower, (420, 340), (530, 490), (680, 495)); tag(lower, dispatchRoutes);
color(dispatchRoutes, dim); stroke(dispatchRoutes, 3); untraced(dispatchRoutes);
rect(worker0, (735, 185), 150, 82);
rect(worker1, (735, 340), 150, 82);
rect(worker2, (735, 495), 150, 82);
for i in 0..3 {
color(worker{i}, panel); outline(worker{i}, cyan); stroke(worker{i}, 2);
}
text(w0, (735, 185), "worker 0");
text(w1, (735, 340), "worker 1");
text(w2, (735, 495), "worker 2");
size(w0, 18); size(w1, 18); size(w2, 18);
livehistogram(destinations, (1030, 350), 0, 3, 3, 330, 300, magenta);
text(histTitle, (1030, 170), "DESTINATION OUTCOME");
size(histTitle, 19); bold(histTitle); color(histTitle, dim);
counter(arrived, (1030, 555), 0, 0, "arrived ", " / 54");
size(arrived, 21); color(arrived, dim);
text(caption, (cx, 650),
"The diagram and histogram are two views of the same seeded dispatch.");
size(caption, 21); color(caption, dim);
hidden(requests); hidden(destinations); hidden(arrived); hidden(caption);
step("network") {
par {
draw(dispatchRoutes, 0.75);
show(requests, 0.35);
show(destinations, 0.45);
show(arrived, 0.35);
show(caption, 0.35);
}
}
wait(0.35);
step("dispatch") {
par {
branch(requests, dispatchRoutes, 5.0, smooth);
observe(destinations, requests, outcome);
observe(arrived, requests, arrived);
flow(dispatchRoutes, 5.0, forward, continuous);
}
}
wait(1.0);
galton-board-process
One uncertain fork becomes eight left-or-right choices, then 180 persistent balls reveal why many more routes terminate near the center. The same real arrivals build the live bell-shaped histogram before a creator CTA closes the probability story.
// GALTON BOARD — RANDOM LOCALLY, PREDICTABLE GLOBALLY
//
// This is a probability story built from generic process vocabulary. Ordinary
// tagged lines form the board; `branch` preserves each ball through eight
// choices; `collect` and `observe` build the distribution from real arrivals.
title("How Random Choices Become a Bell Curve");
canvas("9:16");
template("blank");
watermark(mark,(w*0.16,h*0.042),"Made With Manic");
text(kicker,(cx,h*0.070),"PROBABILITY · RANDOM LOCALLY, ORDERED GLOBALLY");
text(headline,(cx,h*0.115),"Can random choices create a predictable shape?");
text(chapter,(cx,h*0.195),"1 · BEGIN WITH ONE FORK");
text(caption,(cx,h*0.855),"One ball can land almost anywhere.");
text(insight,(cx,h*0.815),"MORE ROUTES LEAD TO THE CENTER");
text(cta,(cx,h*0.930),"MAKE PROBABILITY VISIBLE → 8gwifi.org/manic");
size(kicker,18); color(kicker,dim); bold(kicker); hidden(kicker);
size(headline,31); bold(headline); wrap(headline,w*0.84); hidden(headline);
size(chapter,19); color(chapter,cyan); bold(chapter); hidden(chapter);
size(caption,21); color(caption,dim); wrap(caption,w*0.84); hidden(caption);
size(insight,19); color(insight,lime); bold(insight); hidden(insight);
size(cta,21); color(cta,cyan); bold(cta); hidden(cta);
equation(law,(cx,h*0.158),`X\sim\operatorname{Binomial}\!\left(8,\frac12\right)`,29);
hidden(law);
let levels = 8;
let boardX = cx;
let topY = h*0.235;
let dx = w*0.065;
let dy = h*0.034;
// Every directed edge joins one row to the next. Converging endpoints create
// the ordinary rooted DAG followed by `branch`.
for r in 0..levels {
for k in 0..r+1 {
let x1 = boardX + (k-r*0.5)*dx;
let y1 = topY + r*dy;
let xl = boardX + (k-(r+1)*0.5)*dx;
let xr = boardX + (k+1-(r+1)*0.5)*dx;
let y2 = topY + (r+1)*dy;
line(left{r}_{k},(x1,y1),(xl,y2));
line(right{r}_{k},(x1,y1),(xr,y2));
tag(left{r}_{k},boardRoutes); tag(right{r}_{k},boardRoutes);
color(left{r}_{k},dim); color(right{r}_{k},dim);
stroke(left{r}_{k},1.7); stroke(right{r}_{k},1.7);
opacity(left{r}_{k},0.30); opacity(right{r}_{k},0.30);
dot(peg{r}_{k},(x1,y1),4.8);
color(peg{r}_{k},fg); glow(peg{r}_{k},0.36); tag(peg{r}_{k},pegs);
}
}
untraced(boardRoutes);
for k in 0..levels+1 {
let tx = boardX + (k-levels*0.5)*dx;
let ty = topY + levels*dy;
dot(exit{k},(tx,ty),5);
color(exit{k},gold); glow(exit{k},0.45); tag(exit{k},exits);
counter(bin{k},(tx,ty+28),k,0);
size(bin{k},15); color(bin{k},dim); tag(bin{k},exitLabels);
}
text(leftChoice,(boardX-dx*0.72,topY+dy*0.72),"LEFT");
text(rightChoice,(boardX+dx*0.72,topY+dy*0.72),"RIGHT");
size(leftChoice,15); size(rightChoice,15);
color(leftChoice,cyan); color(rightChoice,magenta);
hidden(leftChoice); hidden(rightChoice);
circle(source,(boardX,topY),13);
opacity(source,0);
particles(balls,source,180,4.0,73);
color(balls,cyan); glow(balls,0.78); z(balls,9);
let histY = h*0.695;
livehistogram(outcomes,(cx,histY),0,9,9,w*0.76,h*0.155,magenta);
text(histTitle,(cx,h*0.595),"WHERE 180 BALLS ACTUALLY LANDED");
size(histTitle,18); bold(histTitle); color(histTitle,dim);
counter(landed,(cx,h*0.785),0,0,"landed "," / 180");
size(landed,21); color(landed,dim);
hidden(pegs); hidden(exits); hidden(exitLabels);
hidden(balls); hidden(outcomes); hidden(histTitle); hidden(landed);
step("introduce one uncertain choice") {
par {
show(kicker,0.30);
show(headline,0.45);
show(law,0.45);
show(chapter,0.35);
show(caption,0.40);
draw(boardRoutes,0.90);
show(pegs,0.55);
show(exits,0.45);
show(exitLabels,0.45);
show(leftChoice,0.35);
show(rightChoice,0.35);
}
}
wait(0.60);
step("repeat the choice eight times") {
par {
show(outcomes,0.50);
show(histTitle,0.35);
show(landed,0.35);
say(chapter,"2 · REPEAT LEFT OR RIGHT EIGHT TIMES",0.38);
say(caption,"At every peg, each ball makes another equally likely left-or-right choice.",0.44);
}
}
wait(0.55);
step("let the crowd reveal the pattern") {
par {
branch(balls,boardRoutes,8.20,smooth);
collect(outcomes,balls,outcome,0.34,smooth);
observe(outcomes,balls,outcome);
observe(landed,balls,arrived);
show(balls,0.15);
seq {
say(chapter,"3 · WATCH 180 INDIVIDUAL JOURNEYS",0.38);
say(caption,"One route is unpredictable. The crowd begins to expose a stable pattern.",0.44);
wait(3.40);
say(caption,"Every bar is measured from the same balls you see falling—not animated separately.",0.44);
}
}
}
wait(0.65);
step("explain why the center wins") {
par {
pulse(outcomes.bars,0.80);
show(insight,0.45);
say(chapter,"4 · ORDER EMERGES FROM MANY CHOICES",0.38);
say(caption,"Extreme bins need nearly all-left or all-right. Many more mixed sequences end near the center.",0.48);
}
}
wait(0.75);
step("create with Manic") {
par {
pulse(outcomes.bars,0.75);
show(cta,0.45);
say(caption,"Describe the choices once. Manic keeps every route, arrival, count, and live distribution connected.",0.45);
}
}
wait(1.45);
hue_wave
An animated hue wave across a grid.
// Hue Wave — a ring of dots, each with its own starting hue, all advancing
// their hue at the same rate so the rainbow *rotates* around the ring. Shows
// off `hue` as an animatable track: `to(id, hue, degrees)` cycles colour over
// time (unlike `recolor`, it travels around the colour wheel, not through grey).
//
// manic examples/hue_wave.manic
// manic examples/hue_wave.manic --record out --fps 60
title("Hue Wave");
canvas(1280, 720);
text(head, (640, 110), "an animated hue track — colour that cycles");
display(head); color(head, cyan); size(head, 26); hidden(head);
let n = 36; let cx = 640; let cy = 400; let r = 210;
// a ring of dots, rainbow-coloured by angle
for i in 0..n {
let a = tau * i / n;
dot(d{i}, (cx + r*cos(a), cy + r*sin(a)), 18);
hue(d{i}, 360 * i / n);
glow(d{i}, 1.4);
tag(d{i}, ring);
}
// --- script ---
show(head, 0.5);
// spin the whole rainbow: every dot advances its hue by 720 deg (two full
// cycles) over 6s, in parallel — the pattern rotates around the ring
par {
for i in 0..n {
to(d{i}, hue, 360*i/n + 720, 6.0, linear);
}
}
hill_run
A little scene animated with the language layer.
// Uphill / Downhill — a rate x time = distance word problem.
// "Up a hill at 4 mph, back down the same path at 6 mph, round trip = 1 hour.
// Total distance?" Answer: one-way d = 2.4 mi, round trip = 4.8 mi.
//
// The distance is SOLVED in-language: d = 1 / (1/4 + 1/6) = 2.4, total = 2d.
// The runner climbs slowly, descends faster (3s vs 2s ~ the real 0.6h : 0.4h),
// then the equation is derived and the answer counts up on a live readout.
//
// manic examples/hill_run.manic
// manic examples/hill_run.manic --record out --fps 60
title("Uphill / Downhill");
canvas("16:9");
// --- the numbers, computed the same way you'd reason it out ---
let up = 4; // mph, uphill
let down = 6; // mph, downhill
let d = 1 / (1/up + 1/down); // one-way distance = 2.4 mi (from d/4 + d/6 = 1)
let total = 2 * d; // round trip = 4.8 mi
text(head, (cx, 84), "up at 4 mph, down at 6 mph -- round trip takes 1 hour");
display(head); color(head, cyan); size(head, 24); hidden(head);
text(cap, (cx, 668), ""); color(cap, dim); size(cap, 23);
// --- the hill (a single path, run up then down) ---
line(ground, (150, 560), (700, 560)); color(ground, dim); stroke(ground, 2); untraced(ground);
line(path, (200, 560), (620, 210)); color(path, cyan); stroke(path, 4); untraced(path);
text(flag, (628, 196), "top"); color(flag, dim); size(flag, 18); hidden(flag);
dot(runner, (200, 560), 16); color(runner, lime); glow(runner, 1.7); hidden(runner);
text(uplbl, (300, 470), "4 mph"); color(uplbl, cyan); size(uplbl, 24); hidden(uplbl);
text(downlbl, (520, 320), "6 mph"); color(downlbl, magenta); size(downlbl, 24); hidden(downlbl);
// --- the derivation, on the right ---
text(e1, (960, 230), "time = distance / rate"); color(e1, dim); size(e1, 22); hidden(e1);
text(e2, (960, 300), "d/4 + d/6 = 1"); display(e2); color(e2, fg); size(e2, 30); hidden(e2);
text(e3, (960, 360), "5d/12 = 1 -> d = 2.4"); display(e3); color(e3, cyan); size(e3, 26); hidden(e3);
counter(ans, (960, 450), 0, 1, "round trip = 2d = ", " mi"); display(ans); color(ans, lime); size(ans, 30); hidden(ans);
// --- script ---
show(head, 0.5);
say(cap, "an athlete runs up a hill, then back down the same path");
par { draw(ground, 0.5); draw(path, 0.7); }
par { show(flag, 0.3); show(runner, 0.3); }
wait(0.3);
section("Up the hill");
say(cap, "uphill at 4 mph -- the slow leg");
show(uplbl, 0.3);
move(runner, (620, 210), 3.0, linear);
section("Back down");
say(cap, "downhill at 6 mph -- faster, so less time");
show(downlbl, 0.3);
move(runner, (200, 560), 2.0, linear);
wait(0.3);
section("Set up the equation");
say(cap, "let d = the one-way distance; time = distance / rate");
show(e1, 0.4);
show(e2, 0.4);
say(cap, "combine the fractions: 5d/12 = 1, so d = 2.4 miles");
show(e3, 0.5);
flash(e3, lime);
section("Total distance");
say(cap, "the round trip is 2d");
show(ans, 0.3);
to(ans, value, total, 1.4);
pulse(ans);
wait(1.6);
walk
An articulated stick figure walking down a road — legs swing, arms counter-swing, the body
bobs — built purely from the language layer (let + for + trig), no character rig.
title("A Generic Figure Walking Down the Road");
canvas("16:9");
let groundY = cy + 160;
let startX = cx - 420;
let stepDist = 15;
let swingAmp = 26;
let bobAmp = 10;
// ================= road =================
rect(road, (0, groundY), w, h - groundY);
color(road, dim);
filled(road);
untraced(road);
line(roadLine, (0, groundY + 40), (w, groundY + 40));
color(roadLine, panel);
stroke(roadLine, 2);
untraced(roadLine);
for i in 0..12 {
rect(dash{i}, (i*120 - 40, groundY + 36), 50, 8);
color(dash{i}, fg);
filled(dash{i});
untraced(dash{i});
}
// ================= stick figure as points + reflowing segments =================
point(neck, (startX, groundY - 118));
point(hip, (startX, groundY - 10));
point(handL, (startX - 30, groundY - 40));
point(handR, (startX + 30, groundY - 40));
point(footL, (startX - 30, groundY + 100));
point(footR, (startX + 30, groundY + 100));
hidden(neck);
hidden(hip);
hidden(handL);
hidden(handR);
hidden(footL);
hidden(footR);
circle(head, (startX, groundY - 140), 22);
color(head, fg);
outlined(head);
stroke(head, 3);
untraced(head);
segment(spine, neck, hip);
segment(armL, neck, handL);
segment(armR, neck, handR);
segment(legL, hip, footL);
segment(legR, hip, footR);
color(spine, fg);
color(armL, cyan);
color(armR, cyan);
color(legL, gold);
color(legR, gold);
stroke(spine, 4);
stroke(armL, 4);
stroke(armR, 4);
stroke(legL, 4);
stroke(legR, 4);
untraced(spine);
untraced(armL);
untraced(armR);
untraced(legL);
untraced(legR);
// ================= text =================
text(head_label, (cx, 55), "A Generic Figure Walking Down the Road");
color(head_label, cyan);
hidden(head_label);
text(caption, (cx, h - 30), "");
color(caption, dim);
hidden(caption);
// ================= script =================
show(head_label, 0.6);
wait(0.3);
par {
draw(road, 0.5);
draw(roadLine, 0.5);
stagger(0.03) {
for i in 0..12 {
draw(dash{i}, 0.1);
}
}
}
par {
show(neck, 0.01); show(hip, 0.01);
show(handL, 0.01); show(handR, 0.01);
show(footL, 0.01); show(footR, 0.01);
draw(head, 0.4);
draw(spine, 0.3);
draw(armL, 0.3);
draw(armR, 0.3);
draw(legL, 0.3);
draw(legR, 0.3);
}
wait(0.3);
show(caption, 0.4);
say(caption, "Camera pulls back to see the whole road");
par {
cam((cx, cy), 1.0, smooth);
zoom(0.85, 1.0, smooth);
}
wait(0.3);
// --- walk cycle: phase steps by 90 deg so sin actually alternates ---
for i in 0..28 {
let baseX = startX + i*stepDist;
let phase = i*90;
let swing = swingAmp*sin(phase*pi/180);
let legLift = bobAmp*abs(sin(phase*pi/180));
par {
move(neck, (baseX, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip, (baseX, groundY - 10), 0.15, smooth);
move(handL, (baseX - swing, groundY - 40), 0.15, smooth);
move(handR, (baseX + swing, groundY - 40), 0.15, smooth);
move(footL, (baseX + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR, (baseX - swing, groundY + 100 - legLift), 0.15, smooth);
move(head, (baseX, groundY - 140 - legLift*0.4), 0.15, smooth);
}
}
wait(0.2);
say(caption, "Camera zooms in as the figure gets close");
par {
cam((startX + 420, groundY - 80), 1.4, smooth);
zoom(2.2, 1.4, smooth);
}
wait(0.4);
say(caption, "A close-up look, then pulling back out");
par {
cam((cx, cy), 1.2, smooth);
zoom(1, 1.2, smooth);
}
wait(0.4);
show(caption, 0.3);
say(caption, "A generic stick figure walking -- no specific person depicted");
two_person_walk
Two figures walk toward each other, MEET in the middle, shake hands, then continue past — a little choreographed scene from loops and arithmetic alone (the language layer as animation).
title("Two Figures Meet, Shake Hands, and Continue Walking");
canvas("16:9");
let groundY = cy + 160;
let startX1 = cx - 420;
let startX2 = cx + 420;
let stepDist = 15;
let swingAmp = 26;
let bobAmp = 10;
let meetX = cx;
let endX1 = cx + 420;
let endX2 = cx - 420;
// ================= road =================
rect(road, (0, groundY), w, h - groundY);
color(road, dim);
filled(road);
untraced(road);
line(roadLine, (0, groundY + 40), (w, groundY + 40));
color(roadLine, panel);
stroke(roadLine, 2);
untraced(roadLine);
for i in 0..14 {
rect(dash{i}, (i*120 - 40, groundY + 36), 50, 8);
color(dash{i}, fg);
filled(dash{i});
untraced(dash{i});
}
// ================= figure 1 (walks left -> right) =================
point(neck1, (startX1, groundY - 118));
point(hip1, (startX1, groundY - 10));
point(handL1, (startX1 - 30, groundY - 40));
point(handR1, (startX1 + 30, groundY - 40));
point(footL1, (startX1 - 30, groundY + 100));
point(footR1, (startX1 + 30, groundY + 100));
hidden(neck1); hidden(hip1);
hidden(handL1); hidden(handR1);
hidden(footL1); hidden(footR1);
circle(head1, (startX1, groundY - 140), 22);
color(head1, fg);
outlined(head1);
stroke(head1, 3);
untraced(head1);
segment(spine1, neck1, hip1);
segment(armL1, neck1, handL1);
segment(armR1, neck1, handR1);
segment(legL1, hip1, footL1);
segment(legR1, hip1, footR1);
color(spine1, fg);
color(armL1, cyan);
color(armR1, cyan);
color(legL1, gold);
color(legR1, gold);
stroke(spine1, 4); stroke(armL1, 4); stroke(armR1, 4);
stroke(legL1, 4); stroke(legR1, 4);
untraced(spine1); untraced(armL1); untraced(armR1);
untraced(legL1); untraced(legR1);
// ================= figure 2 (walks right -> left, mirrored) =================
point(neck2, (startX2, groundY - 118));
point(hip2, (startX2, groundY - 10));
point(handL2, (startX2 - 30, groundY - 40));
point(handR2, (startX2 + 30, groundY - 40));
point(footL2, (startX2 - 30, groundY + 100));
point(footR2, (startX2 + 30, groundY + 100));
hidden(neck2); hidden(hip2);
hidden(handL2); hidden(handR2);
hidden(footL2); hidden(footR2);
circle(head2, (startX2, groundY - 140), 22);
color(head2, fg);
outlined(head2);
stroke(head2, 3);
untraced(head2);
segment(spine2, neck2, hip2);
segment(armL2, neck2, handL2);
segment(armR2, neck2, handR2);
segment(legL2, hip2, footL2);
segment(legR2, hip2, footR2);
color(spine2, fg);
color(armL2, magenta);
color(armR2, magenta);
color(legL2, lime);
color(legR2, lime);
stroke(spine2, 4); stroke(armL2, 4); stroke(armR2, 4);
stroke(legL2, 4); stroke(legR2, 4);
untraced(spine2); untraced(armL2); untraced(armR2);
untraced(legL2); untraced(legR2);
// ================= text =================
text(head_label, (cx, 55), "Two Figures Meet, Shake Hands, and Continue Walking");
color(head_label, cyan);
hidden(head_label);
text(caption, (cx, h - 30), "");
color(caption, dim);
hidden(caption);
// ================= script =================
show(head_label, 0.6);
wait(0.3);
par {
draw(road, 0.5);
draw(roadLine, 0.5);
stagger(0.03) {
for i in 0..14 {
draw(dash{i}, 0.1);
}
}
}
par {
show(neck1, 0.01); show(hip1, 0.01);
show(handL1, 0.01); show(handR1, 0.01);
show(footL1, 0.01); show(footR1, 0.01);
draw(head1, 0.4);
draw(spine1, 0.3);
draw(armL1, 0.3);
draw(armR1, 0.3);
draw(legL1, 0.3);
draw(legR1, 0.3);
show(neck2, 0.01); show(hip2, 0.01);
show(handL2, 0.01); show(handR2, 0.01);
show(footL2, 0.01); show(footR2, 0.01);
draw(head2, 0.4);
draw(spine2, 0.3);
draw(armL2, 0.3);
draw(armR2, 0.3);
draw(legL2, 0.3);
draw(legR2, 0.3);
}
wait(0.3);
show(caption, 0.4);
say(caption, "Camera pulls back to see the whole road");
par {
cam((cx, cy), 1.0, smooth);
zoom(0.85, 1.0, smooth);
}
wait(0.3);
// --- walk cycle: both figures walk toward each other, meeting at meetX ---
for i in 0..24 {
let baseX1 = startX1 + i*stepDist;
let baseX2 = startX2 - i*stepDist;
let phase = i*90;
let swing = swingAmp*sin(phase*pi/180);
let legLift = bobAmp*abs(sin(phase*pi/180));
par {
move(neck1, (baseX1, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip1, (baseX1, groundY - 10), 0.15, smooth);
move(handL1, (baseX1 - swing, groundY - 40), 0.15, smooth);
move(handR1, (baseX1 + swing, groundY - 40), 0.15, smooth);
move(footL1, (baseX1 + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR1, (baseX1 - swing, groundY + 100 - legLift), 0.15, smooth);
move(head1, (baseX1, groundY - 140 - legLift*0.4), 0.15, smooth);
move(neck2, (baseX2, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip2, (baseX2, groundY - 10), 0.15, smooth);
move(handL2, (baseX2 - swing, groundY - 40), 0.15, smooth);
move(handR2, (baseX2 + swing, groundY - 40), 0.15, smooth);
move(footL2, (baseX2 + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR2, (baseX2 - swing, groundY + 100 - legLift), 0.15, smooth);
move(head2, (baseX2, groundY - 140 - legLift*0.4), 0.15, smooth);
}
}
wait(0.2);
say(caption, "They arrive face to face");
par {
cam((meetX, groundY - 80), 1.2, smooth);
zoom(1.8, 1.2, smooth);
}
// settle into a standing pose facing each other
par {
move(neck1, (meetX - 40, groundY - 118), 0.3, smooth);
move(hip1, (meetX - 40, groundY - 10), 0.3, smooth);
move(footL1, (meetX - 60, groundY + 100), 0.3, smooth);
move(footR1, (meetX - 20, groundY + 100), 0.3, smooth);
move(head1, (meetX - 40, groundY - 140), 0.3, smooth);
move(handL1, (meetX - 70, groundY - 40), 0.3, smooth);
move(neck2, (meetX + 40, groundY - 118), 0.3, smooth);
move(hip2, (meetX + 40, groundY - 10), 0.3, smooth);
move(footL2, (meetX + 60, groundY + 100), 0.3, smooth);
move(footR2, (meetX + 20, groundY + 100), 0.3, smooth);
move(head2, (meetX + 40, groundY - 140), 0.3, smooth);
move(handR2, (meetX + 70, groundY - 40), 0.3, smooth);
}
wait(0.3);
say(caption, "Reaching out to shake hands");
par {
move(handR1, (meetX - 5, groundY - 55), 0.4, smooth);
move(handL2, (meetX + 5, groundY - 55), 0.4, smooth);
}
wait(0.2);
par {
move(handR1, (meetX, groundY - 55), 0.25, smooth);
move(handL2, (meetX, groundY - 55), 0.25, smooth);
}
wait(0.2);
say(caption, "Shaking hands");
for i in 0..4 {
par {
move(handR1, (meetX, groundY - 65), 0.12, smooth);
move(handL2, (meetX, groundY - 65), 0.12, smooth);
}
par {
move(handR1, (meetX, groundY - 48), 0.12, smooth);
move(handL2, (meetX, groundY - 48), 0.12, smooth);
}
}
par {
move(handR1, (meetX, groundY - 55), 0.15, smooth);
move(handL2, (meetX, groundY - 55), 0.15, smooth);
}
wait(0.3);
flash(handR1, gold);
flash(handL2, gold);
wait(0.3);
say(caption, "Letting go and continuing on their separate ways");
par {
cam((cx, cy), 1.2, smooth);
zoom(1, 1.2, smooth);
}
// release hands back to normal swing position before resuming walk
par {
move(handR1, (meetX - 40 + 30, groundY - 40), 0.25, smooth);
move(handL2, (meetX + 40 - 30, groundY - 40), 0.25, smooth);
}
wait(0.2);
// --- resume walk cycle: figure1 continues toward endX1, figure2 toward endX2 ---
for i in 0..24 {
let baseX1 = (meetX - 40) + i*stepDist;
let baseX2 = (meetX + 40) - i*stepDist;
let phase = i*90;
let swing = swingAmp*sin(phase*pi/180);
let legLift = bobAmp*abs(sin(phase*pi/180));
par {
move(neck1, (baseX1, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip1, (baseX1, groundY - 10), 0.15, smooth);
move(handL1, (baseX1 - swing, groundY - 40), 0.15, smooth);
move(handR1, (baseX1 + swing, groundY - 40), 0.15, smooth);
move(footL1, (baseX1 + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR1, (baseX1 - swing, groundY + 100 - legLift), 0.15, smooth);
move(head1, (baseX1, groundY - 140 - legLift*0.4), 0.15, smooth);
move(neck2, (baseX2, groundY - 118 - legLift*0.4), 0.15, smooth);
move(hip2, (baseX2, groundY - 10), 0.15, smooth);
move(handL2, (baseX2 - swing, groundY - 40), 0.15, smooth);
move(handR2, (baseX2 + swing, groundY - 40), 0.15, smooth);
move(footL2, (baseX2 + swing, groundY + 100 - legLift), 0.15, smooth);
move(footR2, (baseX2 - swing, groundY + 100 - legLift), 0.15, smooth);
move(head2, (baseX2, groundY - 140 - legLift*0.4), 0.15, smooth);
}
}
wait(0.3);
say(caption, "Two generic stick figures -- no specific persons depicted");
par {
cam((cx, cy), 1.0, smooth);
zoom(0.85, 1.0, smooth);
}
wait(0.4);
equal_cuts
A circle halved again and again (pizza cuts).
// Equal Cuts — a circle sliced into equal pieces, repeatedly doubled:
// 2 → 4 → 8 equal wedges. Each "cut" is a diameter traced across the circle
// at an equal angle. (manic has no sector primitive yet, so cuts are lines.)
//
// manic examples/equal_cuts.manic
// manic examples/equal_cuts.manic --record out --fps 60
title("Equal Cuts");
canvas(1280, 720);
// the circle to divide, centred at (640, 400) with radius 240
circle(pie, (640, 400), 240); stroke(pie, 3);
// four diameters through the centre at 0, 45, 90, 135 degrees.
// revealed in stages, they cut the circle into 2, then 4, then 8 equal pieces.
line(c0, (400, 400), (880, 400)); color(c0, magenta); stroke(c0, 3); untraced(c0); // 0
line(c1, (640, 160), (640, 640)); color(c1, magenta); stroke(c1, 3); untraced(c1); // 90
line(c2, (470, 230), (810, 570)); color(c2, lime); stroke(c2, 3); untraced(c2); // 135
line(c3, (810, 230), (470, 570)); color(c3, lime); stroke(c3, 3); untraced(c3); // 45
text(cap, (640, 690), ""); color(cap, dim); size(cap, 22);
text(count, (1040, 170), ""); color(count, cyan); size(count, 34); bold(count);
// --- cut in half ---
say(cap, "cut the circle in half");
draw(c0, 0.6);
say(count, "2 pieces");
wait(0.5);
// --- cut again: four equal pieces ---
say(cap, "cut again at a right angle — four equal pieces");
draw(c1, 0.6);
say(count, "4 pieces");
wait(0.5);
// --- and again: eight equal pieces ---
say(cap, "and again on both diagonals — eight equal pieces");
par {
draw(c2, 0.6);
draw(c3, 0.6);
}
say(count, "8 pieces");
pulse(pie);
wait(1.2);
archimedes_pi
Bounding pi with inscribed / circumscribed polygons.
// Approximating pi — Archimedes' method (c. 250 BC): inscribe a regular polygon
// in a circle and its perimeter closes in on the circumference. For an n-gon in
// a circle of radius R the perimeter is 2R * n*sin(pi/n), so pi ~ n*sin(pi/n),
// which -> pi as n grows. We sweep n = 6, 24, 96 (Archimedes' own 96-gon) and
// zoom in to see the last polygon nearly kiss the circle.
//
// Uses: a `for` loop per polygon, computed estimates, a live counter, and the
// camera (cam + zoom).
//
// manic examples/archimedes_pi.manic
// manic examples/archimedes_pi.manic --record out --fps 60
title("Approximating pi");
canvas("16:9");
let ox = 440; let oy = 400; let R = 240; // circle centre + radius
// the estimates, computed in-language
let e6 = 6 * sin(pi/6); // 3.000
let e24 = 24 * sin(pi/24); // 3.133
let e96 = 96 * sin(pi/96); // 3.141
text(head, (640, 78), "Archimedes: straight lines closing in on a circle");
display(head); color(head, cyan); size(head, 25); hidden(head);
text(cap, (640, 675), ""); color(cap, dim); size(cap, 22);
// the true circle (the target)
circle(circ, (ox, oy), R); outlined(circ); outline(circ, dim); stroke(circ, 2); untraced(circ);
// live pi readout
counter(est, (990, 330), 0, 3, "pi ~ ", ""); display(est); color(est, lime); size(est, 40); hidden(est);
text(truth, (990, 395), "true pi = 3.14159..."); color(truth, dim); size(truth, 20); hidden(truth);
// --- hexagon: n = 6 (magenta) ---
let n = 6;
for i in 0..n {
let a0 = tau*i/n; let a1 = tau*(i+1)/n;
line(h{i}, (ox + R*cos(a0), oy + R*sin(a0)), (ox + R*cos(a1), oy + R*sin(a1)));
color(h{i}, magenta); stroke(h{i}, 3); untraced(h{i}); tag(h{i}, p6);
}
// --- 24-gon (cyan) ---
let n = 24;
for i in 0..n {
let a0 = tau*i/n; let a1 = tau*(i+1)/n;
line(g{i}, (ox + R*cos(a0), oy + R*sin(a0)), (ox + R*cos(a1), oy + R*sin(a1)));
color(g{i}, cyan); stroke(g{i}, 3); untraced(g{i}); tag(g{i}, p24);
}
// --- 96-gon (lime), Archimedes' own ---
let n = 96;
for i in 0..n {
let a0 = tau*i/n; let a1 = tau*(i+1)/n;
line(k{i}, (ox + R*cos(a0), oy + R*sin(a0)), (ox + R*cos(a1), oy + R*sin(a1)));
color(k{i}, lime); stroke(k{i}, 2); untraced(k{i}); tag(k{i}, p96);
}
// --- script ---
show(head, 0.5);
say(cap, "how close can straight lines get to a curve?");
draw(circ, 1.0);
par { show(est, 0.3); show(truth, 0.3); }
wait(0.4);
section("6 sides");
say(cap, "start with a hexagon inside the circle");
draw(p6, 0.8);
to(est, value, e6, 1.0);
wait(0.7);
fade(p6, 0.4);
section("24 sides");
say(cap, "more sides hug the circle more tightly");
draw(p24, 1.0);
to(est, value, e24, 1.0);
wait(0.7);
fade(p24, 0.4);
section("96 sides");
say(cap, "Archimedes went to 96 sides -- around 250 BC");
draw(p96, 1.2);
to(est, value, e96, 1.0);
pulse(est);
wait(0.7);
section("Almost a circle");
say(cap, "zoom in: the polygon edge and the arc nearly touch");
par { cam((ox, oy - R), 1.5, smooth); zoom(5, 1.5, smooth); }
wait(1.4);
par { cam((cx, cy), 1.0, smooth); zoom(1, 1.0, smooth); }
wait(0.8);
pieday
A Pi Day card: a rainbow petal-flower built from a loop of circles, radial rays,
the digits of π, and the definition circumference / diameter = pi.
title("Pi Day");
canvas("16:9");
let r = h*0.23;
let centerY = cy + 25;
let n = 64;
let petalsN = 12;
text(head, (cx, 70), "Happy Pi Day");
text(bigPi, (cx, centerY - 8), "pi");
text(digits, (cx, h - 92), "3.1415926535897932384626433832795028841971...");
text(formula, (cx, h - 50), "circumference / diameter = pi");
size(head, 40);
size(bigPi, 112);
size(digits, 24);
size(formula, 26);
bold(head);
bold(bigPi);
color(head, magenta);
color(bigPi, gold);
color(digits, cyan);
color(formula, lime);
hidden(head);
hidden(bigPi);
hidden(digits);
hidden(formula);
circle(mainCircle, (cx, centerY), r);
line(diameter, (cx - r, centerY), (cx + r, centerY));
text(diamLab, (cx, centerY + 34), "diameter");
text(circLab, (cx, centerY - r - 30), "circumference");
stroke(mainCircle, 5);
stroke(diameter, 3);
color(mainCircle, cyan);
color(diameter, lime);
color(diamLab, lime);
color(circLab, cyan);
size(diamLab, 22);
size(circLab, 22);
hidden(diamLab);
hidden(circLab);
untraced(mainCircle);
untraced(diameter);
for i in 0..petalsN {
circle(petal{i}, (cx + 0.54*r*cos(tau*i/petalsN), centerY + 0.54*r*sin(tau*i/petalsN)), 0.46r);
stroke(petal{i}, 2);
hue(petal{i}, 360i/petalsN);
opacity(petal{i}, 0.34);
untraced(petal{i});
tag(petal{i}, petals);
}
for i in 0..n {
dot(spark{i}, (cx + 1.23*r*cos(tau*i/n), centerY + 1.23*r*sin(tau*i/n)), 4);
hue(spark{i}, 360*i/n);
hidden(spark{i});
tag(spark{i}, sparks);
}
for i in 0..24 {
line(ray{i}, (cx + 1.02*r*cos(tau*i/24), centerY + 1.02*r*sin(tau*i/24)), (cx + 1.18*r*cos(tau*i/24), centerY + 1.18*r*sin(tau*i/24)));
stroke(ray{i}, 3);
hue(ray{i}, 360*i/24);
untraced(ray{i});
tag(ray{i}, rays);
}
dot(centerDot, (cx, centerY), 6);
color(centerDot, gold);
hidden(centerDot);
show(head, 0.7);
par {
draw(petals, 1.4);
draw(mainCircle, 1.2);
}
par {
draw(diameter, 0.8);
show(centerDot, 0.4);
show(diamLab, 0.5);
show(circLab, 0.5);
}
par {
show(bigPi, 0.9);
draw(rays, 0.9);
}
stagger(0.018) {
for i in 0..n {
show(spark{i}, 0.25);
}
}
par {
show(digits, 0.7);
show(formula, 0.7);
}
pulse(bigPi, 0.8);
pulse(mainCircle, 0.8);
par {
spin(petals, 18, 3.0, smooth);
spin(sparks, -35, 3.0, smooth);
}
wait(1.2);