r/maniclang

▲ 3 r/maniclang+1 crossposts

circuits - manic

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

Manic Animation code

// The circuit kit, in six circuits.
//
// A contact sheet that comes alive: six real circuits on screen from the first frame, then each
// one erases itself, draws itself back, names itself and runs its own current — a different
// shape, colour and pace for every panel. The last beat runs all six at once.
//
// Every dot on screen is a charge integral of a solved branch current, so the six panels are
// running at honestly different speeds because their currents differ, not because six numbers
// were typed. Nothing here is a circuit-specific animation verb: erase, draw, show and par are
// Manic's core kit.

title("six circuits");
canvas("16:9");
template("paper");

text(brand, (640, 40), "maniclang.com");
display(brand);
size(brand, 22);
color(brand, dim);

// ── the board: complete from t = 0, so the first frame is already the whole kit ──

circuit(ohm, (250, 240), `
  dc-voltage 0 4 0 0 v=9
  resistor   0 0 4 0 r=1k
  wire       4 0 4 4
  wire       4 4 0 4
  ground     0 4
`, 40, 1, 0);

circuit(divider, (640, 240), `
  dc-voltage 0 4 0 0 v=9
  resistor   0 0 4 0 r=3k
  resistor   4 0 4 4 r=1k
  wire       4 4 0 4
  ground     0 4
`, 40, 1, 0);

circuit(rc, (1030, 240), `
  dc-voltage 0 4 0 0 v=5
  resistor   0 0 4 0 r=1k
  capacitor  4 0 4 4 c=10u
  wire       4 4 0 4
  ground     0 4
`, 40, 1, 0);

circuit(rl, (250, 512), `
  dc-voltage 0 4 0 0 v=5
  resistor   0 0 4 0 r=100
  inductor   4 0 4 4 l=10m
  wire       4 4 0 4
  ground     0 4
`, 40, 1, 0);

circuit(rect, (640, 512), `
  ac-voltage 0 4 0 0 v=5 f=60
  diode      0 0 3 0
  resistor   3 0 3 4 r=1k
  wire       3 4 0 4
  ground     0 4
`, 40, 1, 0);

circuit(led, (1030, 512), `
  dc-voltage 0 4 0 0 v=5
  resistor   0 0 3 0 r=330
  led        3 0 3 4
  wire       3 4 0 4
  ground     0 4
`, 40, 1, 0);

// ── a different current in every panel: shape, colour, pace ──
//
// Inks chosen for the `paper` template: on cream, gold and amber wash out, so these are the
// darker end of the palette — the colours a textbook would actually print in.

current(ohm, 1, circle, crimson, 3);
current(divider, 1.5, circle, indigo, 3);
current(rc, 1.2, square, green, 3);
current(rl, 2, diamond, purple, 4);
current(rect, 1.6, diamond, orange, 4);
current(led, 2.5, circle, magenta, 4);

// ── the names, which arrive as each panel takes its turn ──

text(n1, (250, 352), "Ohm's law");
hidden(n1);
size(n1, 24);
color(n1, crimson);

text(n2, (640, 352), "voltage divider");
hidden(n2);
size(n2, 24);
color(n2, indigo);

text(n3, (1030, 352), "RC charging");
hidden(n3);
size(n3, 24);
color(n3, green);

text(n4, (250, 624), "RL current rise");
hidden(n4);
size(n4, 24);
color(n4, purple);

text(n5, (640, 624), "half-wave rectifier");
hidden(n5);
size(n5, 24);
color(n5, orange);

text(n6, (1030, 624), "LED + series resistor");
hidden(n6);
size(n6, 24);
color(n6, magenta);

// ── six beats: clear to nothing, draw, name, run ──
//
// The erase has to go to a real zero state, and that means addressing the RIGHT tags. `erase` is
// a stroke verb — it traces a shape out — so on the bare circuit id it would take the component
// strokes away and leave the value labels and the charge dots sitting there, and a text entity
// under `trace` reveals PART of its characters ("10mH" erasing down to "1"). So the strokes are
// erased, and everything that is not a stroke is faded.

wait(0.8);

par {
  erase(ohm.parts, 0.35);
  fade(ohm.labels, 0.3);
  fade(ohm.charge, 0.2);
}
par {
  draw(ohm.parts, 0.85);
  show(ohm.labels, 0.5);
}
show(n1, 0.3);
run(ohm, 2.0);

par {
  erase(divider.parts, 0.35);
  fade(divider.labels, 0.3);
  fade(divider.charge, 0.2);
}
par {
  draw(divider.parts, 0.85);
  show(divider.labels, 0.5);
}
show(n2, 0.3);
run(divider, 2.0);

par {
  erase(rc.parts, 0.35);
  fade(rc.labels, 0.3);
  fade(rc.charge, 0.2);
}
par {
  draw(rc.parts, 0.85);
  show(rc.labels, 0.5);
}
show(n3, 0.3);
run(rc, 2.0);

par {
  erase(rl.parts, 0.35);
  fade(rl.labels, 0.3);
  fade(rl.charge, 0.2);
}
par {
  draw(rl.parts, 0.85);
  show(rl.labels, 0.5);
}
show(n4, 0.3);
run(rl, 2.0);

par {
  erase(rect.parts, 0.35);
  fade(rect.labels, 0.3);
  fade(rect.charge, 0.2);
}
par {
  draw(rect.parts, 0.85);
  show(rect.labels, 0.5);
}
show(n5, 0.3);
run(rect, 2.0);

par {
  erase(led.parts, 0.35);
  fade(led.labels, 0.3);
  fade(led.charge, 0.2);
}
par {
  draw(led.parts, 0.85);
  show(led.labels, 0.5);
}
show(n6, 0.3);
run(led, 2.0);

// ── and the whole board alive at once ──

wait(0.3);
par {
  run(ohm, 5.0);
  run(divider, 5.0);
  run(rc, 5.0);
  run(rl, 5.0);
  run(rect, 5.0);
  run(led, 5.0);
}
wait(0.7);
u/anish2good — 17 hours ago
▲ 18 r/maniclang+1 crossposts

How the Moon Moves Every Ocean - manic

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

Manic Animation code

// tides — how the Moon moves every ocean on Earth, and why the popular sentence is wrong.
//
// "The Moon's gravity pulls on the oceans" predicts ONE bulge and one high tide a day. The
// sea gives two. The fix is the whole subject: what raises a tide is not the pull but the
// DIFFERENCE in pull across the planet — a gradient, and a gradient has two ends.
//
//   ACT I    the fact: two highs a day, every day
//   ACT II   the wrong picture, stated properly before it is demolished
//   ACT III  the gradient — subtract the pull on Earth's centre and a QUADRUPOLE is left.
//            The ocean is 12,000 parcels of water `advect`ed through it, so the two bulges
//            are computed, not drawn. Then the planet turns under them and one coast passes
//            through both: two high tides a day, from one Moon
//   ACT IV   sideways, not up: 1.1 micrometres per second squared, a ten-millionth of
//            gravity. Nothing is lifted; water is herded
//   ACT V    the Sun pulls 179x harder and loses, because tides go as 1/d^3. Add the two
//            and the spring/neap fortnight EMERGES from two cosines
//   ACT VI   the honest part: the real ocean is not two bulges gliding around
//
// The whole film is one stage — the view down on the north pole, so Earth's rotation is an
// ordinary in-plane `turn` and the water can stay where the Moon put it. The tidal field is
// one formula: with the Moon along +x it is (2x, -y). Water cannot leave the surface, so what
// moves it is the TANGENTIAL part — the radial component projected out, F - (F.r)r — which is
// exactly why a tide is a horizontal shove and not a lift.
//
//   manic examples/tides.manic
title("Tides — How the Moon Moves Every Ocean — manic");
canvas("16:9");
template("black");
bloom(0.3, 0.52, 22);

// ---- the mark, up top and above everything, for the whole film ----
text(brand, (640, 32), "maniclang.com");
display(brand); size(brand, 19); color(brand, cyan); opacity(brand, 0.72); z(brand, 100);

// ---- type ----
text(ttl, (640, 92), "How the Moon Moves Every Ocean");
display(ttl); size(ttl, 46); bold(ttl); color(ttl, fg); hidden(ttl);
text(sub, (640, 146), "and why \"it pulls the water up\" is the wrong answer");
display(sub); size(sub, 22); color(sub, dim); hidden(sub);
text(cap, (640, 668), ""); display(cap); size(cap, 23); color(cap, fg); hidden(cap);
text(act, (1060, 624), ""); display(act); size(act, 19); color(act, gold); hidden(act);

// ================================ THE STAGE ================================
// Earth seen from over the north pole, 300 px across, so its surface is at radius 150.
svg(earth, (640, 360), "asset:svg/emoji/1f30e.svg", 300);
circle(orbit, (640, 360), 300); outlined(orbit); outline(orbit, dim);
opacity(orbit, 0.45); hidden(orbit);
svg(moon, (940, 360), "asset:svg/emoji/1f315.svg", 82);
text(moonlab, (940, 470), "the Moon — one lap: 27.3 days");
display(moonlab); size(moonlab, 17); color(moonlab, dim); hidden(moonlab);
// one coast, riding the surface
dot(coast, (640, 210), 9); color(coast, gold); hidden(coast);
text(coastlab, (640, 176), "one coast");
display(coastlab); size(coastlab, 17); color(coastlab, gold); hidden(coastlab);

// a tide gauge: the Moon's own constituent, 12.4206 hours, over two days
coords(gauge, (210, 468), (0, 48), (-1.4, 1.4), 19, 54, 0, 12, 1, "hours", "");
color(gauge, dim); hidden(gauge);
plot(trace, (210, 468), 19, 54, "cos(x/12.4206*tau)", (0, 48));
color(trace, cyan); untraced(trace); hidden(trace);
text(twice, (640, 262), "two highs, every day");
display(twice); size(twice, 21); color(twice, gold); hidden(twice);

// ---- ACT II — the wrong picture ----
for k in 0..7 {
  arrow(pull{k}, (556, 240 + k*40), (700, 240 + k*40));
  color(pull{k}, gold);
  untraced(pull{k});
  tag(pull{k}, pulls);
}
// r(t) = R(1 + e·cos t): one bulge facing the Moon — what "it pulls the water" predicts
param(wrong, (640, 360), 150, 150,
  "(1 + 0.14*cos(t))*cos(t)", "(1 + 0.14*cos(t))*sin(t)", (0, tau));
color(wrong, gold); untraced(wrong);
text(wronglab, (640, 566), "one bulge  ⇒  one high tide a day");
display(wronglab); size(wronglab, 21); color(wronglab, gold); hidden(wronglab);
text(nope, (640, 606), "✗  the sea gives two");
display(nope); size(nope, 21); color(nope, magenta); hidden(nope);

// ---- ACT III — the gradient ----
dot(pnear, (790, 360), 7); color(pnear, gold); hidden(pnear);
dot(pmid, (640, 360), 7); color(pmid, fg); hidden(pmid);
dot(pfar, (490, 360), 7); color(pfar, gold); hidden(pfar);
arrow(gnear, (790, 360), (916, 360)); color(gnear, gold); untraced(gnear);
arrow(gmid, (640, 360), (750, 360)); color(gmid, fg); untraced(gmid);
arrow(gfar, (490, 360), (587, 360)); color(gfar, gold); untraced(gfar);
text(g1, (640, 182), "one pull, unequally felt — gravity falls off as 1/d²");
display(g1); size(g1, 20); color(g1, dim); hidden(g1);
arrow(tnear, (790, 360), (862, 360)); color(tnear, magenta); untraced(tnear);
arrow(tfar, (490, 360), (418, 360)); color(tfar, magenta); untraced(tfar);
text(g2, (640, 182), "subtract the pull on the centre — the whole planet already falls with it");
display(g2); size(g2, 20); color(g2, magenta); hidden(g2);

// THE TIDAL FIELD, tangential part only: F - (F·r̂)r̂ with F = (2x, -y). The epsilon keeps the
// planet's centre finite, where the projection is undefined.
vectorfield(tide, (640, 360), 470, 290,
  "0.42*(2*x - x*(2*x*x - y*y)/(x*x + y*y + 0.02))",
  "0.42*(-y - y*(2*x*x - y*y)/(x*x + y*y + 0.02))", 15);
color(tide, dim); opacity(tide, 0.55); hidden(tide);

// the ocean: a shell of water on the surface, which the field herds
cloud(sea, 12000, cyan, 0.75) {
  let a = (i/12000)*tau;
  let w = mod(i, 7) - 3;
  let x = 640 + (153 + w*1.7)*cos(a);
  let y = 360 + (153 + w*1.7)*sin(a);
  let r = 1.5;
  let hue = 192 + 10*w;
}
glow(sea, 2); hidden(sea);
// and the shape all that herding is heading for: the equilibrium tide, a prolate ellipsoid
// r(t) = R(1 + e(3cos²t − 1)/2) — with e drawn about 200,000x too big to be visible at all
parameter(phi, (150, 600), 0, 0, 0.4, "the Moon has moved on", 2); hidden(phi.widget);
param(bulge, (640, 360), 150, 150,
  "(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*cos(t)",
  "(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*sin(t)", (0, tau));
bind(phi, bulge, formula,
  "(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*cos(t)",
  "(1 + 0.12*(3*cos(t-p)*cos(t-p) - 1)/2)*sin(t)");
color(bulge, gold); untraced(bulge);
text(twolab, (640, 578), "two bulges  ⇒  two high tides a day  ✓");
display(twolab); size(twolab, 21); color(twolab, gold); hidden(twolab);
text(spinlab, (640, 606), "");
display(spinlab); size(spinlab, 20); color(spinlab, cyan); hidden(spinlab);

// ---- ACT IV — how gentle it is ----
equation(tiny, (640, 244), `a_{\text{tide}}=\frac{2GMr}{d^3}=1.1\times10^{-6}\ \mathrm{m/s^2}`, 30);
color(tiny, magenta); hidden(tiny);
equation(vsg, (640, 322), `\frac{a_{\text{tide}}}{g}\approx 10^{-7}`, 32);
color(vsg, gold); hidden(vsg);
text(gentle, (640, 400), "it could not lift a puddle — but it can shove an ocean sideways for six hours");
display(gentle); size(gentle, 20); color(gentle, dim); hidden(gentle);

// ---- ACT V — the Sun loses, and the beat ----
equation(sunpull, (640, 240), `\frac{F_{\odot}}{F_{\text{Moon}}} = 179`, 30);
color(sunpull, gold); hidden(sunpull);
equation(suntide, (640, 328), `\frac{a_{\odot}}{a_{\text{Moon}}} = 0.46`, 30);
color(suntide, cyan); hidden(suntide);
text(cube, (640, 404), "a pull goes as 1/d², a difference in pull as 1/d³ — and the Sun is 390× farther");
display(cube); size(cube, 20); color(cube, dim); hidden(cube);

// two constituents added: the Moon's 12.4206 h and the Sun's 12.000 h. Nothing here sets a
// fortnight — the 14.77-day spring/neap envelope is what two cosines DO.
coords(month, (160, 470), (0, 720), (-1.7, 1.7), 1.34, 46, 0, 168, 1, "hours", "");
color(month, dim); hidden(month);
plot(beat, (160, 470), 1.34, 46, "cos(x/12.4206*tau) + 0.46*cos(x/12*tau)", (0, 720));
color(beat, cyan); untraced(beat); hidden(beat);
text(springlab, (635, 330), "spring");
display(springlab); size(springlab, 18); color(springlab, gold); hidden(springlab);
text(neaplab, (397, 330), "neap");
display(neaplab); size(neaplab, 18); color(neaplab, magenta); hidden(neaplab);
text(fortnight, (640, 602), "14.77 days, spring to spring — and nobody typed that number in");
display(fortnight); size(fortnight, 20); color(fortnight, gold); hidden(fortnight);

// ---- ACT VI — the honest part ----
text(truth1, (640, 244), "The real ocean is not two bulges gliding around a smooth planet.");
display(truth1); size(truth1, 24); color(truth1, fg); hidden(truth1);
text(truth2, (640, 312), "Continents are in the way. Basins ring. The tide turns around fixed nodes.");
display(truth2); size(truth2, 21); color(truth2, dim); hidden(truth2);
text(fundy, (400, 400), "Bay of Fundy:  16 m");
display(fundy); size(fundy, 22); color(fundy, gold); hidden(fundy);
text(med, (890, 400), "much of the Mediterranean:  ~0");
display(med); size(med, 22); color(med, magenta); hidden(med);
text(truth3, (640, 480), "The Moon writes the forcing. The coastline decides the tide.");
display(truth3); size(truth3, 22); color(truth3, cyan); hidden(truth3);

// ================================= ACT I =================================
show(ttl, 0.9);
show(sub, 0.7);
wait(1.3);
show(cap, 0.3);
say(cap, "Every coast on Earth does this twice a day, and has done for four billion years.");
par { fade(ttl, 0.8); fade(sub, 0.8); }
show(earth, 0.9);
par { show(orbit, 0.6); show(moon, 0.7); show(moonlab, 0.5); }
wait(0.4);
say(cap, "One Moon, one lap of us every 27.3 days. Everything that follows comes from that.");
// a rigidly turned label arrives upside down, so it steps off for the lap
fade(moonlab, 0.4);
par {
  turn(moon, (640, 360), 360, 4.2, smooth);
  turn(earth, (640, 360), 90, 4.2, smooth);
}
show(moonlab, 0.4);
wait(0.4);
say(cap, "A tide gauge on any coast, two days of it: high, low, high, low, high.");
// the gauge needs the whole width, so the planet steps out for a moment
par { fade(earth, 0.7); fade(moon, 0.6); fade(moonlab, 0.4); fade(orbit, 0.5); }
// while the stage is dark the Moon takes up its working position, far off to the right,
// where the tidal field's formula puts it
move(moon, (1150, 360), 0.01);
move(moonlab, (1150, 436), 0.01);
say(moonlab, "the Moon");
show(gauge, 0.6);
show(trace, 0.4);
draw(trace, 2.0, smooth);
show(twice, 0.5);
wait(1.4);
say(cap, "Two a day. That number is the whole puzzle — and the usual explanation gets it wrong.");
wait(2.2);

// ================================= ACT II =================================
say(act, "II · the wrong picture");
show(act, 0.4);
par { fade(gauge, 0.6); fade(trace, 0.6); fade(twice, 0.5); }
say(cap, "The Moon's gravity pulls on the oceans. So far, so true.");
par { show(earth, 0.8); show(moon, 0.6); show(moonlab, 0.4); }
wait(0.5);
stagger(0.07) {
  for k in 0..7 {
    draw(pull{k}, 0.5);
  }
}
wait(1.2);
say(cap, "Pull the water toward the Moon and it heaps up on the near side. One heap.");
draw(wrong, 1.2, smooth);
show(wronglab, 0.5);
wait(1.8);
say(cap, "Which is one high tide a day. The sea gives two. Something is missing.");
show(nope, 0.6);
wait(2.2);

// ================================= ACT III =================================
say(act, "III · the difference, not the pull");
par { fade(pulls, 0.6); fade(wrong, 0.6); fade(wronglab, 0.5); fade(nope, 0.5); }
say(cap, "Three places: the near side, the centre, the far side.");
par { show(pnear, 0.4); show(pmid, 0.4); show(pfar, 0.4); }
wait(0.7);
say(cap, "Gravity weakens with distance, so those three pulls are not the same size.");
show(g1, 0.5);
stagger(0.22) {
  draw(gnear, 0.6);
  draw(gmid, 0.6);
  draw(gfar, 0.6);
}
wait(1.8);
say(cap, "But the planet is already falling around its centre. Subtract that pull from all three.");
par { fade(g1, 0.5); fade(gnear, 0.5); fade(gmid, 0.5); fade(gfar, 0.5); }
show(g2, 0.6);
par { draw(tnear, 0.7); draw(tfar, 0.7); }
wait(1.8);
say(cap, "What is left points AWAY at both ends. A gradient has two ends. There is the two.");
wait(2.2);
say(cap, "Do that at every point at once, and this is the field the Moon leaves behind.");
par { fade(g2, 0.5); fade(tnear, 0.5); fade(tfar, 0.5); fade(pnear, 0.4); fade(pmid, 0.4); fade(pfar, 0.4); }
show(tide, 0.9);
wait(1.4);
say(cap, "Now put twelve thousand parcels of water on the surface and let the field push them.");
show(sea, 0.8);
wait(0.5);
advect(sea, tide, 6.5, 0.55);
wait(0.4);
say(cap, "Nothing was placed by hand. The water went where the arrows converge — and there are two.");
draw(bulge, 1.4, smooth);
show(twolab, 0.6);
wait(2.2);
// the payoff: the water stays where the Moon put it, and the planet turns underneath
say(cap, "The bulges belong to the Moon, not to the planet. So turn the planet underneath them.");
// the field's arrows are fixed to the Moon's OLD direction, so they bow out before it moves
par { show(coast, 0.5); show(coastlab, 0.4); fade(sea, 0.8); fade(tide, 0.8); }
wait(0.9);
say(spinlab, "one rotation = one day");
show(spinlab, 0.4);
// one day: Earth turns once, and the Moon does not wait — it moves on 360/27.3 = 13.2 degrees,
// taking the tide's axis with it
par {
  fade(coastlab, 0.5);
  turn(earth, (640, 360), 360, 7.0, linear);
  turn(coast, (640, 360), 360, 7.0, linear);
  turn(moon, (640, 360), 13.2, 7.0, linear);
  to(phi, value, 0.23, 7.0, linear);
}
say(cap, "One coast, one day, two bulges crossed. Two high tides — and the water never travelled.");
wait(2.2);
// and the reason tide tables slip: the coast is back where it started, the Moon is not
say(cap, "But look: the coast is home and the Moon has moved on thirteen degrees.");
say(spinlab, "the Moon moved on 13° while the planet turned once");
wait(2.0);
say(cap, "So the coast has to chase it — about fifty minutes more of turning, every single day.");
par {
  turn(earth, (640, 360), 13.2, 1.6, smooth);
  turn(coast, (640, 360), 13.2, 1.6, smooth);
}
say(spinlab, "one tidal day = 24 h 50 min");
wait(2.4);

// ================================= ACT IV =================================
say(act, "IV · sideways, not up");
// the caption turns over with the stage, so no frame is left empty under a stale line
say(cap, "One more correction, and it is the one that surprises people. Look how gentle this is.");
par {
  fade(coast, 0.4); fade(spinlab, 0.4); fade(bulge, 0.6); fade(twolab, 0.5);
  fade(earth, 0.8); fade(moon, 0.6); fade(moonlab, 0.4);
}
show(tiny, 0.8);
wait(1.6);
show(vsg, 0.7);
say(cap, "A ten-millionth of the gravity holding that ocean down.");
wait(1.8);
show(gentle, 0.6);
say(cap, "Nothing gets lifted. Water gets nudged SIDEWAYS for six hours, and arrives.");
wait(2.4);

// ================================= ACT V =================================
say(act, "V · the Sun loses");
par { fade(tiny, 0.7); fade(vsg, 0.7); fade(gentle, 0.6); }
say(cap, "The Sun pulls Earth a hundred and seventy-nine times harder than the Moon does.");
show(sunpull, 0.8);
wait(1.6);
say(cap, "And raises less than half the tide, because a DIFFERENCE falls off faster than a pull.");
show(suntide, 0.8);
show(cube, 0.6);
wait(2.4);
par { fade(sunpull, 0.6); fade(suntide, 0.6); fade(cube, 0.5); }
say(cap, "So the ocean answers two clocks: 12.42 hours for the Moon, 12.00 for the Sun.");
show(month, 0.6);
show(beat, 0.4);
draw(beat, 3.0, smooth);
wait(0.6);
say(cap, "Add them. Where the two clocks agree the tides run big; where they fight, small.");
par { show(springlab, 0.5); show(neaplab, 0.5); }
wait(1.6);
say(cap, "Spring tides, neap tides — a fortnight apart, out of two cosines and nothing else.");
show(fortnight, 0.7);
wait(2.4);

// ================================= ACT VI =================================
say(act, "VI · the honest part");
par {
  fade(month, 0.6); fade(beat, 0.6); fade(springlab, 0.4); fade(neaplab, 0.4);
  fade(fortnight, 0.6);
}
show(truth1, 0.8);
say(cap, "Everything so far is the FORCING. It is not the tide you can go and watch.");
wait(1.8);
show(truth2, 0.7);
say(cap, "Water cannot chase the Moon around a planet with two continents in the way.");
wait(1.8);
par { show(fundy, 0.6); show(med, 0.6); }
say(cap, "One bay rings like an organ pipe and swings sixteen metres. Another barely moves.");
wait(2.2);
show(truth3, 0.8);
wait(2.4);

// ================================= ENDCARD =================================
par {
  fade(truth1, 0.7); fade(truth2, 0.6); fade(truth3, 0.7);
  fade(fundy, 0.5); fade(med, 0.5); fade(cap, 0.6); fade(act, 0.5);
}
text(end1, (640, 318), "The Moon does not lift the sea.");
display(end1); size(end1, 40); bold(end1); color(end1, fg); hidden(end1);
text(end2, (640, 380), "It pulls one side harder than the other.");
display(end2); size(end2, 30); color(end2, cyan); hidden(end2);
text(end3, (640, 458), "and the ocean charges interest: 3.8 cm of Moon a year, paid out of Earth's spin");
display(end3); size(end3, 19); color(end3, dim); hidden(end3);
text(end4, (640, 522), "— manic");
display(end4); size(end4, 24); color(end4, gold); hidden(end4);
show(end1, 0.9);
show(end2, 0.8);
show(end3, 0.7);
show(end4, 0.6);
wait(2.6);
u/anish2good — 2 days ago
▲ 2 r/maniclang+1 crossposts

Spirals Nature Keeps Reusing — Fibonacci, Vogel, Fermat, Curlicue & the Uzumaki — manic

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

Manic Animation code

// 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);
u/anish2good — 1 day ago
▲ 40 r/maniclang+1 crossposts

Crinoid — combing the current - manic

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

Manic Animation code

// cloud-crinoid — another  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 u/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);
u/anish2good — 3 days ago
▲ 76 r/maniclang+3 crossposts

How a neural network learns: gradient descent - manic

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

Manic Animation code

title("How a neural network learns: gradient descent");
canvas("16:9");
template("black");

camera3((6, -8.5, 5.5), (0, 0, 0.7), 40);
axes3(world, (0, 0, 0), 2.4);
surface3(f, "exp(-x*x + cos(2*y))", (-2, 2), (-2, 2), 34);
opacity(f, 0.42);

slice3(sx, f, x, 0.6, 0.4, #ff3ea5);   // hold x → cross-section along y (∂f/∂y)
slice3(sy, f, y, 0.4, 0.6, #ffb020);   // hold y → cross-section along x (∂f/∂x)
gradient3(grad, f, 0.6, 0.4);
descend3(desc, f, 0.7, 0.35, 0.25, 45, #7cff3e);

equation(feq, (232, 96), `f(x,y)=e^{-x^2+\cos 2y}`, 28);
equation(ceq, (232, 150), `\text{cost } C(w_1,\dots,w_n)`, 24); color(ceq, dim);
equation(geq, (1040, 632), `\nabla f=\begin{bmatrix}\partial f/\partial x\\ \partial f/\partial y\end{bmatrix}`, 28);
text(narr, (640, 668), "A network's cost is one number over millions of weights.", 24);

hidden(sx); hidden(sx.slope);
hidden(sy); hidden(sy.slope);
hidden(grad); hidden(geq); hidden(narr);
hidden(desc); hidden(desc.ball);
untraced(desc); untraced(desc.ball);   // start un-drawn so the descent can roll on

// A MOVIE camera runs concurrently with the story: it swings around and up so the
// stacked slices + gradient are never hidden behind the peak, then drops low to
// follow the ball rolling into the valley. (orbit3 composes in `par`.)
par {
  seq {
    orbit3(-18, 26, 9.5, 5.0, smooth);   // establish: front-left, slightly high
    orbit3(40, 36, 9.2, 6.5, smooth);    // swing right & rise to reveal the STACK
    orbit3(62, 22, 8.4, 6.0, smooth);    // drop low, come round to watch the descent
    orbit3(74, 24, 8.4, 4.5, smooth);    // settle on the valley
  }
  seq {
    wait(0.5);
    show(narr);
    show(ceq);
    wait(2.2);

    say(narr, "Picture it as a landscape over just two of them.");
    wait(2.0);

    say(narr, "To read ∂f/∂y, hold x constant — that slices the surface to one curve.");
    par { show(sx); show(sx.slope); }
    wait(2.4);

    say(narr, "Hold y constant instead, and this slice's steepness is ∂f/∂x.");
    par { show(sy); show(sy.slope); }
    wait(2.4);

    say(narr, "Stack the two partials and you get the gradient — steepest ascent.");
    par { show(grad); show(geq); }
    wait(2.8);

    say(narr, "To LEARN, step the other way: downhill, along −∇f.");
    par { show(desc); show(desc.ball); }
    par { draw(desc, 3.0); draw(desc.ball, 3.0); }   // the ball rolls, the trail draws
    wait(0.6);

    say(narr, "It rolls into a valley — a minimum of the cost. That is learning.");
    pulse(desc.ball);
    wait(2.6);
  }
}
u/anish2good — 6 days ago
▲ 10 r/maniclang+4 crossposts

A fractal dawn, and a murmuration - manic

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

Manic Animation code

// 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);
u/anish2good — 7 days ago
▲ 69 r/maniclang+3 crossposts

Vectors, basis, and transformations - manic

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

Manic Animation code

// ============================================================================
//  Essence of Linear Algebra, chapters 1→2→3 in one arc.
//
//  Ch1  A vector is an arrow from the origin; its coordinates are [x, y].
//  Ch2  Those coordinates are SCALARS: v = x·î + y·ĵ — a linear combination of
//       the basis vectors î=(1,0), ĵ=(0,1).
//  Ch3  A matrix is a transformation: it says where î and ĵ LAND, and the whole
//       grid morphs to follow (`gridmap` animates it). Because v = x·î + y·ĵ,
//       it rides along, landing at x·(new î) + y·(new ĵ).
//
//  Example: v = [1, 2] under [[2,1],[1,2]] (î→(2,1), ĵ→(1,2)), so v lands at
//  1·(2,1) + 2·(1,2) = (4,5). Origin (400,470), 55 px/unit.
// ============================================================================
title("Vectors, basis, and transformations");
canvas(1280, 720);
template("black");

text(narr, (640, 696), "A vector is an arrow from the origin.", 24);

// the plane + basis, ready to be transformed in Ch3 (starts at the identity)
gridmap(gm, (400, 470), 55, 2, 1, 1, 2, 5);

// the vector v = [1,2] and its linear-combination scaffold
vector(v, (400, 470), (55, 110), yellow);
vector(jj, (455, 470), (0, 110), red);           // 2·j-hat, stacked on i-hat → reaches v
equation(vlab, (486, 352), `\vec{v}=[1,2]`, 30); color(vlab, yellow);
equation(ilab, (472, 500), `\hat{\imath}`, 26); color(ilab, lime);
equation(jlab, (366, 414), `\hat{\jmath}`, 26); color(jlab, red);
equation(mtx, (980, 165), `\begin{bmatrix}2&1\\1&2\end{bmatrix}`, 44);

// Ch4 — a second transform (90° rotation) composed on top: morph M1 → M2·M1
gridmap(gm2, (400, 470), 55, -1, -2, 2, 1, 5, 2, 1, 1, 2);
equation(comp4, (640, 96), `\begin{bmatrix}0&-1\\1&0\end{bmatrix}\begin{bmatrix}2&1\\1&2\end{bmatrix}=\begin{bmatrix}-1&-2\\2&1\end{bmatrix}`, 34);

// the determinant cell (Ch5), the projection (Ch7), and the alt-basis grid (Ch8)
determinant(dt, (400, 470), 55, 2, 1, 1, 2, gold);
project(pr, (400, 470), 55, (1, 2), (3, 1));       // project v=(1,2) onto span(w=(3,1))
squish(sq, (400, 470), 55, 1, 2, 5);               // Ch7 duality: dot with v = squish onto a line
eigen(ev, (400, 470), 55, 2, 1, 1, 2, gold);       // Ch9 eigenvectors of [[2,1],[1,2]] (λ = 3, 1)
diagonalise(dg, (400, 470), 55, 2, 1, 1, 2, gold); // Ch10 eigenbasis = a pure stretch
coords(cf, (700, 450), (-4, 4), (-3, 3), 46, 46, 0); // Ch11 a function graph…
plot(fn, (700, 450), 46, 46, "0.25*x*x*x - x", (-3.6, 3.6)); color(fn, mint);
equation(deq, (700, 130), `\tfrac{d}{dx}\;\leftrightarrow\;\begin{bmatrix}0&1&0\\0&0&2\\0&0&0\end{bmatrix}`, 32);
gridmap(cb, (400, 470), 55, 1, 0.7, 0.4, 1, 5);
vector(vb, (400, 470), (55, 110), yellow);

// --- initial visibility ---
hidden(gm.i); hidden(gm.j);                       // basis revealed in Ch2
hidden(v); hidden(vlab); hidden(jj); hidden(ilab); hidden(jlab); hidden(mtx);
hidden(dt); hidden(dt.unit); hidden(dt.val); hidden(pr);
hidden(sq); hidden(sq.line); hidden(sq.dual);
hidden(ev); hidden(dg); hidden(cf); hidden(fn); hidden(deq);
hidden(gm2); hidden(gm2.bg); hidden(gm2.i); hidden(gm2.j); hidden(comp4);
hidden(cb); hidden(cb.bg); hidden(cb.i); hidden(cb.j); hidden(vb);

// --- Ch1: a vector ---
wait(0.5);
par { show(v); show(vlab); }
wait(1.3);

// --- Ch2: coordinates are a linear combination of the basis ---
say(narr, "Its coordinates are scalars: v = 1·i-hat + 2·j-hat.");
par { show(gm.i); show(gm.j); show(ilab); show(jlab); }
wait(0.7);
show(jj);                                         // i-hat then two j-hats reach v
wait(1.6);

// --- Ch3: the matrix transforms space; v rides along ---
say(narr, "A matrix moves i-hat and j-hat — and the whole grid follows.");
show(mtx);
par { fade(jj); fade(ilab); fade(jlab); fade(vlab); }
par { to(gm, morph, 1, 2.2); grow(v, (620, 195), 2.2); }   // space deforms, v follows
wait(0.6);
say(narr, "v lands at 1·(new i-hat) + 2·(new j-hat) = (4,5).");
wait(1.6);

// --- Ch4: matrix multiplication is composition ---
say(narr, "Ch 4 — apply one transform, then another: that is matrix multiplication.");
show(comp4);                                       // the product, clearly placed up top
par { fade(gm); fade(v); }
show(gm2);                                          // starts at M1 (seamless with Ch3)
to(gm2, morph, 1, 1.8);                             // now rotate 90°: the grid is at M2·M1
wait(1.3);
to(gm2, morph, 0, 1.5);                             // undo the second transform, back to M1
par { fade(gm2); fade(comp4); }
par { show(gm); show(v); }
wait(0.6);

// --- Ch5: the determinant is the area scale factor ---
say(narr, "Ch 5 — the determinant is how much areas scale: 2·2 − 1·1 = 3.");
par { show(dt); show(dt.unit); show(dt.val); }
wait(1.9);
par { fade(dt); fade(dt.unit); fade(dt.val); }    // fade = timeline hide (hidden is base-state only)

// --- Ch6: column space + the inverse undoes the transform ---
say(narr, "Ch 6 — the columns span the whole plane, so the inverse sends space back.");
par { to(gm, morph, 0, 1.8); grow(v, (455, 360), 1.8); }   // reverse the morph = the inverse
wait(1.4);

// --- Ch7: the dot product — projection, and its dual (a squish onto a line) ---
say(narr, "Ch 7 — the dot product of v and w is a projection — w's shadow on v.");
par { fade(gm); fade(gm.bg); fade(v); fade(mtx); }
show(pr);
wait(1.6);
say(narr, "It's also a squish of the whole plane onto a line — the dual vector IS v.");
fade(pr);
par { show(sq); show(sq.line); show(sq.dual); }
to(sq, morph, 1, 1.9);                              // collapse space onto the number line
wait(1.0);
to(sq, morph, 0, 1.2);                              // and back — the dual vector remains
wait(0.8);

// --- Ch8: change of basis ---
say(narr, "Ch 8 — change of basis: the same arrow, read on a different grid.");
par { fade(sq); fade(sq.line); fade(sq.dual); }
par { show(cb); show(vb); }
to(cb, morph, 1, 1.8);                                       // the coordinate grid changes; v stays put
wait(1.8);

// --- Ch9: eigenvectors and eigenvalues ---
say(narr, "Ch 9 — some vectors keep to their own line: eigenvectors (λ = 3 and λ = 1).");
par { fade(cb); fade(vb); }
show(ev);
wait(2.0);

// --- Ch10: the eigenbasis is a pure stretch ---
say(narr, "Ch 10 — in the eigenbasis, the transformation is just a diagonal stretch.");
fade(ev);
show(dg);
wait(2.0);

// --- Ch11: abstract vector spaces ---
say(narr, "Ch 11 — even functions are vectors; the derivative is a linear map with a matrix.");
fade(dg);
par { show(cf); show(fn); show(deq); }
wait(2.2);
u/anish2good — 9 days ago
▲ 16 r/maniclang+2 crossposts

Area of a circle = πr² - manic

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

Manic Animation code

// 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);
u/anish2good — 8 days ago
▲ 62 r/maniclang+1 crossposts

An orrery — a clockwork solar system - manic

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

Manic Animation code

// 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);
  }
}
u/anish2good — 10 days ago
▲ 25 r/maniclang+3 crossposts

quantum-double-slit — one particle, two slits, ONE field, three faces - manic

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

Manic Animation code

// quantum-double-slit — one particle, two slits, ONE field, three faces.
//
// The whole experiment's physics is authored ONCE, as a `field`:
//   |psi|^2(x,y) = sinc^2( single-slit diffraction )  ·  cos^2( two-slit interference )
// the EXACT Fraunhofer double-slit intensity (sinc^2 envelope with its real nulls /
// "missing orders", not a gaussian fudge). That single `field(psi2,…)` then drives all
// three views — a per-pixel heatmap, the detection screen (dots + curve), and a 3-D
// amplitude surface — so they are provably the SAME expression, not three drifting models.
//
// Act I shows the animated REAL wave Re(psi) rolling through the slits (a live wavelength
// slider tightens the fringes — Δy ∝ λ). That real ripple is honestly distinguished from
// what a detector records: the intensity |psi|^2. Equations shown are all exact:
//   ψ = ψ1 + ψ2 ,   I = |ψ1+ψ2|^2 ≠ |ψ1|^2 + |ψ2|^2 ,   Δy = λL/d.
//
//   manic examples/quantum-double-slit.manic
title("One Particle, Two Slits");
canvas("16:9");
template("black");
bloom(0.32, 0.55, 30);

// ===== the physics, authored ONCE (slits at (0, ±0.5), screen sampled at x = 4.5) =====
field(psi2, "sinc(8.0*y/hypot(x+0.15, y))^2 * cos(24.0*(hypot(x, y-0.5) - hypot(x, y+0.5)))^2");

parameter(lam, (w - 175, 150), 0.55, 0.30, 0.90, "wavelength", 2);

// ---- Act I: the animated REAL wave Re(psi) — plane wave -> two slits -> superposition ----
shader(wave) {
  let k = 6.2832 / lam;
  let wv = 6.0;
  let px = (u - 0.5) * asp * 5.0 + 1.5;      // world x  (slit plane at x=0)
  let py = (v - 0.5) * 6.0;                   // world y  (transverse)
  let m1 = step(0.0, px);
  let r1 = hypot(px, py - 0.5) + 0.001;
  let r2 = hypot(px, py + 0.5) + 0.001;
  let a1 = cos(k*r1 - wv*t) / sqrt(1.0 + 1.4*r1);
  let a2 = cos(k*r2 - wv*t) / sqrt(1.0 + 1.4*r2);
  let pl = 0.72 * cos(k*px - wv*t);
  let re = (1.0 - m1)*pl + 0.8*m1*(a1 + a2);
  let s = tanh(1.6 * re);
  let r = 0.34 + 0.42*s;
  let g = 0.18 + 0.10*abs(s);
  let b = 0.66 - 0.34*s;
}
z(wave, -10);

// ---- the |psi|^2 heatmap of the SAME field (revealed after the wave settles) ----
shader(field2) {
  let px = (u - 0.5) * asp * 5.0 + 1.5;
  let py = (v - 0.5) * 6.0;
  let d = psi2(px, py);                        // <-- the SAME field
  let r = 0.08 + 1.20*d;
  let g = 0.05 + 0.55*d*d;
  let b = 0.26 + 0.75*d;
}
z(field2, -9); hidden(field2);

// ---- the barrier with two slits (world x=0 -> ~424px; slits at world y=±0.5) ----
rect(wal1, (424, 150), 12, 300); tag(wal1, wall);
rect(wal2, (424, 360), 12, 96);  tag(wal2, wall);
rect(wal3, (424, 570), 12, 300); tag(wal3, wall);
line(scr, (1130, 70), (1130, 650)); stroke(scr, 3); color(scr, gold); untraced(scr);
text(scrl, (1130, 52), "screen"); size(scrl, 20); color(scrl, gold); plate(scrl); hidden(scrl);

// ---- Act II: the detection screen (dots + curve), sampled from the SAME field ----
// dot i survives iff hash(i) < |psi|^2 at its transverse position — honest rejection
// sampling against the field, at the screen line x=4.5.
cloud(hits, 4200, #ffffff, 0.95) {
  let rn = mod(abs(sin(i * 12.9898) * 43758.55), 1);
  let rn2 = mod(abs(sin(i * 78.233) * 12543.7), 1);
  let rn3 = mod(abs(sin(i * 45.164) * 9631.4), 1);
  let ys = mod(i * 0.61803, 1) * 5.0 - 2.5;                 // transverse position
  let inten = psi2(4.5, ys);                                // <-- the SAME field
  let survive = step(rn, inten);
  let ap = smoothstep(0, 0.2, t - (24.5 + 12.0 * rn2));
  let x = 640 + ys * 150;
  let y = 360 + (rn3 - 0.5) * 380;
  let r = 2.0 * survive * ap;
  let hue = 45 + 20 * rn;
}
hidden(hits);
plot(icur, (cx, 620), 150, 150, "psi2(4.5, x)", (-2.5, 2.5));
untraced(icur); stroke(icur, 3); gradient(icur, blue, gold, 270);

// ---- Act III: the SAME field as a 3-D amplitude surface ----
camera3((6.4, -6.6, 4.4), (2.3, 0, 0.3), 44);
surface3(surf, "psi2(x, y)", (0.35, 5.0), (-2.6, 2.6), 96); hidden(surf); color(surf, gold);

// ---- HUD ----
text(head, (cx, 52), "One Particle, Two Slits"); size(head, 36); color(head, white); glow(head, 6); plate(head); display(head); cursor(head);
text(cap, (cx, h - 40), ""); size(cap, 24); color(cap, white); plate(cap);
equation(eqQ, (cx, 128), `\psi \;=\; \psi_1 + \psi_2`, 30); plate(eqQ); hidden(eqQ);
counter(nhit, (w - 175, 250), 0, 0, "detections ", ""); color(nhit, gold); hidden(nhit);

// ================= timeline =================

// ---- Act I: the wave meets the wall
type(head, 1.1);
show(scrl, 0.4);
draw(scr, 0.7);
say(cap, "a quantum wave rolls toward a wall with two openings", 0.6);
show(eqQ, 0.6);
wait(2.4);
say(cap, "beyond the slits: TWO waves - from ONE particle - overlapping", 0.5);
wait(2.6);
cue(tick);
say(cap, "shorter wavelength, tighter fringes - the spacing goes as lambda", 0.5);
to(lam, value, 0.36, 2.0, smooth);
wait(1.0);
to(lam, value, 0.6, 1.6, smooth);
wait(0.8);

// ---- the honest reframe: real wave vs. what is measured
cue(tick);
say(cap, "but this rippling is the REAL part of the wave - a detector never sees it", 0.6);
wait(2.0);
rewrite(eqQ, `I \;=\; |\psi|^{2}`, 0.9);
say(cap, "what it records is the INTENSITY - one field, |psi| squared", 0.6);
par { show(field2, 1.3); fade(wave, 1.3); }
wait(1.4);

// ---- Act II: one particle at a time, drawn from that field
cue(whoosh);
say(cap, "now face the screen - send the particles through ONE AT A TIME", 0.5);
par { fade(field2, 0.9); fade(wall, 0.8); fade(scr, 0.7); fade(scrl, 0.7); fade(lam, 0.7); }
par { show(hits, 0.4); show(nhit, 0.3); }
cue(tick);
rewrite(eqQ, `I \;=\; |\psi_1+\psi_2|^{2} \;\neq\; |\psi_1|^{2}+|\psi_2|^{2}`, 0.9);
par {
  to(nhit, value, 1600, 15, linear);
  seq {
    say(cap, "each electron lands at a single point - apparently at random...", 0.5);
    wait(5.0);
    say(cap, "...yet none EVER land in the dark bands - not even the diffraction nulls", 0.5);
    wait(4.5);
    say(cap, "the stripes assemble - one self-interfering particle at a time", 0.5);
    wait(3.5);
  }
}
draw(icur, 1.6);
cue(chime);
say(cap, "every dot is drawn from that ONE field - it traces |psi| squared exactly", 0.6);
wait(2.2);

// ---- Act III: the same field, as a landscape
cue(whoosh);
par { fade(hits, 0.9); fade(icur, 0.7); fade(nhit, 0.6); }
par { show(surf, 1.2); }
say(cap, "the same field a third time - now as terrain: peaks bright, valleys silent", 0.6);
par { to(nhit, value, 1600, 0.1, linear); orbit3(-26, 26, 8.6, 6, smooth); }
say(cap, "along the nodal rays the two waves cancel forever - flat valleys of never", 0.6);
cue(tick);
rewrite(eqQ, `\boxed{\;\Delta y \;=\; \dfrac{\lambda L}{d}\;}`, 1.0);
par { orbit3(20, 18, 8.0, 5, smooth); breathe(eqQ, 3, 0.05, 0, 5); }
say(cap, "one particle, two paths, one field - the same physics in every view", 0.7);
wait(2.6);
u/anish2good — 11 days ago
▲ 38 r/maniclang+3 crossposts

Calculus on a Live Shadertoy Ocean - manic

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

Manic Animation code

// glsl-derivative-wave — "manic meets Shadertoy": a real Shadertoy (TDM's raymarched
// Seascape) runs as a LIVE backdrop the whole time, while a Calculus-1 lesson plays on
// top of it — the DERIVATIVE as the slope of the tangent line, taught as "the slope of
// a wave". A tangent slides along y = sin x; its slope is positive climbing, ZERO at the
// crest (the water is momentarily flat - a maximum), negative descending, zero again at
// the trough. The payoff reveals f'(x) = cos x crossing zero exactly at those peaks.
// Everything is 2D over the shader, so the ocean never leaves the frame.
// Shader: https://www.shadertoy.com/view/Ms2SD1 (Alexander Alekseev, CC BY-NC-SA 3.0)
//
//   manic examples/glsl-derivative-wave.manic
title("manic meets Shadertoy - the slope of a wave");
canvas(1280, 720);
template("black");

// ================= the live Shadertoy (TDM's Seascape, raw glsl) =================
glsl(sea, `
const int NUM_STEPS = 32;
const float PI         = 3.141592;
const float EPSILON    = 1e-3;
#define EPSILON_NRM (0.1 / iResolution.x)
const int ITER_GEOMETRY = 3;
const int ITER_FRAGMENT = 5;
const float SEA_HEIGHT = 0.6;
const float SEA_CHOPPY = 4.0;
const float SEA_SPEED = 0.8;
const float SEA_FREQ = 0.16;
const vec3 SEA_BASE = vec3(0.0,0.09,0.18);
const vec3 SEA_WATER_COLOR = vec3(0.8,0.9,0.6)*0.6;
#define SEA_TIME (1.0 + iTime * SEA_SPEED)
const mat2 octave_m = mat2(1.6,1.2,-1.2,1.6);
mat3 fromEuler(vec3 ang) {
    vec2 a1 = vec2(sin(ang.x),cos(ang.x));
    vec2 a2 = vec2(sin(ang.y),cos(ang.y));
    vec2 a3 = vec2(sin(ang.z),cos(ang.z));
    mat3 m;
    m[0] = vec3(a1.y*a3.y+a1.x*a2.x*a3.x,a1.y*a2.x*a3.x+a3.y*a1.x,-a2.y*a3.x);
    m[1] = vec3(-a2.y*a1.x,a1.y*a2.y,a2.x);
    m[2] = vec3(a3.y*a1.x*a2.x+a1.y*a3.x,a1.x*a3.x-a1.y*a3.y*a2.x,a2.y*a3.y);
    return m;
}
float hash( vec2 p ) { float h = dot(p,vec2(127.1,311.7)); return fract(sin(h)*43758.5453123); }
float noise( in vec2 p ) {
    vec2 i = floor( p ); vec2 f = fract( p );
    vec2 u = f*f*(3.0-2.0*f);
    return -1.0+2.0*mix( mix( hash( i + vec2(0.0,0.0) ), hash( i + vec2(1.0,0.0) ), u.x),
                mix( hash( i + vec2(0.0,1.0) ), hash( i + vec2(1.0,1.0) ), u.x), u.y);
}
float diffuse(vec3 n,vec3 l,float p) { return pow(dot(n,l) * 0.4 + 0.6,p); }
float specular(vec3 n,vec3 l,vec3 e,float s) {
    float nrm = (s + 8.0) / (PI * 8.0);
    return pow(max(dot(reflect(e,n),l),0.0),s) * nrm;
}
vec3 getSkyColor(vec3 e) {
    e.y = (max(e.y,0.0)*0.8+0.2)*0.8;
    return vec3(pow(1.0-e.y,2.0), 1.0-e.y, 0.6+(1.0-e.y)*0.4) * 1.1;
}
float sea_octave(vec2 uv, float choppy) {
    uv += noise(uv);
    vec2 wv = 1.0-abs(sin(uv)); vec2 swv = abs(cos(uv));
    wv = mix(wv,swv,wv);
    return pow(1.0-pow(wv.x * wv.y,0.65),choppy);
}
float map(vec3 p) {
    float freq = SEA_FREQ; float amp = SEA_HEIGHT; float choppy = SEA_CHOPPY;
    vec2 uv = p.xz; uv.x *= 0.75;
    float d, h = 0.0;
    for(int i = 0; i < ITER_GEOMETRY; i++) {
        d = sea_octave((uv+SEA_TIME)*freq,choppy);
        d += sea_octave((uv-SEA_TIME)*freq,choppy);
        h += d * amp; uv *= octave_m; freq *= 1.9; amp *= 0.22;
        choppy = mix(choppy,1.0,0.2);
    }
    return p.y - h;
}
float map_detailed(vec3 p) {
    float freq = SEA_FREQ; float amp = SEA_HEIGHT; float choppy = SEA_CHOPPY;
    vec2 uv = p.xz; uv.x *= 0.75;
    float d, h = 0.0;
    for(int i = 0; i < ITER_FRAGMENT; i++) {
        d = sea_octave((uv+SEA_TIME)*freq,choppy);
        d += sea_octave((uv-SEA_TIME)*freq,choppy);
        h += d * amp; uv *= octave_m; freq *= 1.9; amp *= 0.22;
        choppy = mix(choppy,1.0,0.2);
    }
    return p.y - h;
}
vec3 getSeaColor(vec3 p, vec3 n, vec3 l, vec3 eye, vec3 dist) {
    float fresnel = clamp(1.0 - dot(n, -eye), 0.0, 1.0);
    fresnel = min(fresnel * fresnel * fresnel, 0.5);
    vec3 reflected = getSkyColor(reflect(eye, n));
    vec3 refracted = SEA_BASE + diffuse(n, l, 80.0) * SEA_WATER_COLOR * 0.12;
    vec3 color = mix(refracted, reflected, fresnel);
    float atten = max(1.0 - dot(dist, dist) * 0.001, 0.0);
    color += SEA_WATER_COLOR * (p.y - SEA_HEIGHT) * 0.18 * atten;
    color += specular(n, l, eye, 600.0 * inversesqrt(dot(dist,dist)));
    return color;
}
vec3 getNormal(vec3 p, float eps) {
    vec3 n;
    n.y = map_detailed(p);
    n.x = map_detailed(vec3(p.x+eps,p.y,p.z)) - n.y;
    n.z = map_detailed(vec3(p.x,p.y,p.z+eps)) - n.y;
    n.y = eps;
    return normalize(n);
}
float heightMapTracing(vec3 ori, vec3 dir, out vec3 p) {
    float tm = 0.0; float tx = 1000.0;
    float hx = map(ori + dir * tx);
    if(hx > 0.0) { p = ori + dir * tx; return tx; }
    float hm = map(ori);
    for(int i = 0; i < NUM_STEPS; i++) {
        float tmid = mix(tm, tx, hm / (hm - hx));
        p = ori + dir * tmid;
        float hmid = map(p);
        if(hmid < 0.0) { tx = tmid; hx = hmid; } else { tm = tmid; hm = hmid; }
        if(abs(hmid) < EPSILON) break;
    }
    return mix(tm, tx, hm / (hm - hx));
}
vec3 getPixel(in vec2 coord, float time) {
    vec2 uv = coord / iResolution.xy;
    uv = uv * 2.0 - 1.0;
    uv.x *= iResolution.x / iResolution.y;
    vec3 ang = vec3(sin(time*3.0)*0.1,sin(time)*0.2+0.3,time);
    vec3 ori = vec3(0.0,3.5,time*5.0);
    vec3 dir = normalize(vec3(uv.xy,-2.0)); dir.z += length(uv) * 0.14;
    dir = normalize(dir) * fromEuler(ang);
    vec3 p;
    heightMapTracing(ori,dir,p);
    vec3 dist = p - ori;
    vec3 n = getNormal(p, dot(dist,dist) * EPSILON_NRM);
    vec3 light = normalize(vec3(0.0,1.0,0.8));
    return mix(getSkyColor(dir), getSeaColor(p,n,light,dir,dist),
               pow(smoothstep(0.0,-0.02,dir.y),0.2));
}
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
    float time = iTime * 0.3;
    vec3 color = getPixel(fragCoord, time);
    fragColor = vec4(pow(color,vec3(0.65)), 1.0);
}
`);
z(sea, -20);

// subdue the sea so the bright graph pops (still shimmering underneath)
rect(scrim, (640, 360), 1280, 720); color(scrim, #04070b); opacity(scrim, 0.42); z(scrim, -15);
// cinematic bands for the title & captions
rect(topband, (640, 46), 1280, 132); color(topband, #05070a); opacity(topband, 0.5); z(topband, -9);
rect(botband, (640, 684), 1280, 76);  color(botband, #05070a); opacity(botband, 0.5); z(botband, -9);

// ---- HUD ----
text(head, (640, 42), "manic meets Shadertoy"); size(head, 40); color(head, white); glow(head, 6); display(head); cursor(head);
text(sub, (640, 86), "the derivative - the slope of a wave"); size(sub, 20); color(sub, #bfe6ef); hidden(sub);
text(cap, (640, 684), ""); size(cap, 22); color(cap, white);

// worksheet chip (dark glass) — the symbolic ladder f -> f' -> f''
rect(chip, (1075, 172), 330, 224); color(chip, #081420); opacity(chip, 0.6); z(chip, -8);
equation(eqf, (1075, 112), `f(x)=\sin x`, 25); hidden(eqf);
equation(eqfp, (1075, 168), `f'(x)=\cos x`, 25); hidden(eqfp);
equation(eqfpp, (1075, 224), `f''(x)=-\sin x`, 25); hidden(eqfpp);

// ---- the wave and the tangent that reads its slope ----
let gx = 150;
let gy = 392;
arrow(xax, (gx - 30, gy), (gx + 6.3*150 + 30, gy)); untraced(xax); stroke(xax, 2); color(xax, #9fb6c4); tag(xax, g2);
plot(wave, (gx, gy), 150, 125, "sin(x)", (0, 6.3));
untraced(wave); stroke(wave, 5); color(wave, #7fe0ff); glow(wave, 5);
// the derivative curve f'(x)=cos x — revealed in Act 1
deriv(dv, wave, #ff7bd0); untraced(dv); dashed(dv, 10, 8);
// the SECOND derivative f''(x)=-sin x (derivative of the derivative) — Act 2
deriv(dv2, dv, #ffb14e); untraced(dv2); dashed(dv2, 4, 8);
// extrema (crest & trough) and inflection points (concavity flips)
extrema(ext, wave, gold); hidden(ext);
inflections(infl, wave, #74f7a0); hidden(infl);
// the tangent line + its LIVE slope readout, both riding the same x
tangent(tang, wave, 0.35, 300); color(tang, gold); stroke(tang, 4); glow(tang, 4); hidden(tang);
slope(slp, wave, 0.35); color(slp, gold); hidden(slp);

// ================= timeline =================

// ---- establish: a live Shadertoy, then calculus on it
type(head, 1.0);
show(sub, 0.5);
say(cap, "this whole sea is one real Shadertoy - now let's do calculus on it", 0.9);
wait(0.4);

// ---- the wave is a function
par { draw(xax, 0.6); show(eqf, 0.5); }
draw(wave, 1.6);
say(cap, "a wave is just a function - here y = sin x", 0.8);
par { show(tang, 0.5); show(slp, 0.4); }
say(cap, "the derivative is the SLOPE of the tangent line - how steep the water is here", 0.9);
wait(0.4);

// ---- climb to the crest: slope positive -> zero
say(cap, "climbing the front of the wave - the slope is positive", 0.8);
par { to(tang, x, 1.5708, 1.8); to(slp, x, 1.5708, 1.8); }
say(cap, "at the crest the water is momentarily FLAT - the slope is zero", 1.0);
pulse(slp, 0.8);
wait(0.5);

// ---- down to the trough: slope negative -> zero
say(cap, "over the top and down - now the slope is negative", 0.9);
par { to(tang, x, 4.7124, 2.2); to(slp, x, 4.7124, 2.2); }
say(cap, "at the trough it's flat again - slope zero, a minimum", 0.9);
pulse(slp, 0.8);
wait(0.4);

// ---- f' as a curve: zero slope marks the peaks; reveal f'(x)=cos x
show(ext, 0.6);
say(cap, "zero slope marks every peak and every trough", 0.8);
show(eqfp, 0.5);
draw(dv, 1.6);
say(cap, "and there is f'(x) = cos x - it crosses zero exactly at those points", 1.0);
wait(0.6);

// ---- Act 2: peak or valley? the SECOND derivative decides
say(cap, "but f'(x)=0 only finds the flat spots - which is a peak, which a valley?", 1.1);
say(cap, "differentiate AGAIN: f''(x) is the curvature - how the slope itself changes", 1.1);
show(eqfpp, 0.5);
draw(dv2, 1.6);
wait(0.3);

// second-derivative test at the crest
par { show(tang, 0.4); show(slp, 0.4); to(tang, x, 1.5708, 0.9); to(slp, x, 1.5708, 0.9); }
say(cap, "at the crest f'' < 0: the wave arches over - concave down - a MAXIMUM", 1.2);
pulse(dv2, 0.7);
wait(0.4);

// second-derivative test at the trough
par { to(tang, x, 4.7124, 1.3); to(slp, x, 4.7124, 1.3); }
say(cap, "at the trough f'' > 0: it cups upward - concave up - a MINIMUM", 1.2);
pulse(dv2, 0.7);
wait(0.4);

// inflection points where concavity flips (f''=0)
show(infl, 0.6);
say(cap, "and where f''=0 - the zero-crossings - the curve flips: inflection points", 1.2);
pulse(infl, 0.8);
wait(0.5);

// ---- close: the whole ladder, sin -> cos -> -sin
fade(tang, 0.5);
say(cap, "position, slope, curvature - sin, cos, minus sin - one wave, fully read", 1.2);
say(cap, "real calculus on a real shader - manic meets Shadertoy", 1.1);
wait(2.6);
u/anish2good — 14 days ago