Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 parameter draws 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-spiral shader vortex 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/sdroundboxsmin- 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);

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