Physics — simulations
Each simulation is pre-simulated with RK4 at build time — deterministic and replayable — and its parts are ordinary manic entities the whole language composes with. The phase / time / well / energy views are optional and generic: any sim inherits them.
Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).
pendulum
One pendulum shown four ways from a single deterministic swing: the motion (with a
velocity arrow + KE/PE bars), the phase portrait (θ vs ω), a time series, the
potential-energy well, and energy over time (pendulum + phase/timegraph/
well/energygraph + swing).
// ============================================================================
// pendulum.manic — the physics kit's first sim, seen FOUR ways (Layer 1)
// ----------------------------------------------------------------------------
// `pendulum(id, [center], [length], [angle0], [unit], [damping])` builds a
// pendulum from its physics — PRE-SIMULATED with RK4 at build time
// (deterministic). Only `id` is required. The OPTIONAL, generic view builtins
// render the SAME simulation as math panels and all animate together on `swing`:
// · phase(id,(cx,cy),[size]) — phase portrait (θ vs ω): a closed loop
// · timegraph(id,(cx,cy),[size]) — θ(t) & ω(t) with a sweep line
// · well(id,(cx,cy),[size]) — energy well U(θ), bob = ball in a bowl
// · energygraph(id,(cx,cy),[size]) — KE / PE / total over time
//
// TO ADAPT: add damping (6th arg) and watch the phase loop spiral inward, the
// ball settle to the bottom of the well, and total energy decay — e.g.
// `pendulum(p, (250,220), 1.2, 55, 105, 0.5)`.
// ============================================================================
title("Pendulum — one swing, four views");
canvas("16:9");
text(hdr, (cx, 40), "One pendulum, four views");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the physical sim (left), with velocity arrow + KE/PE bars
pendulum(p, (250, 220), 1.2, 55, 105);
untraced(p.path);
// four math views of the SAME simulation, 2×2 on the right
phase(p, (715, 165), 90); // θ vs ω — a closed loop
timegraph(p, (1000, 165), 90); // θ(t), ω(t) with a sweep line
well(p, (715, 455), 90); // U(θ) with the bob as a rolling ball
energygraph(p, (1000, 455), 90); // KE / PE / total over time
text(cap, (cx, h - 30), "sim · phase portrait · time series · energy well · energy over time — all from one pre-simulated swing");
size(cap, 16); color(cap, dim); display(cap);
draw(p.path, 0.8);
swing(p, 10); // every panel animates in lockstep
second-law-thermodynamics
Why entropy only grows, in five vignettes: gas particles mixing into far more microstates, heat flowing until two sides equalize, free expansion into new volume, a heat engine that must dump waste heat, and the reversible-vs-irreversible limit.
// ============================================================================
// second-law-thermodynamics.manic — entropy, irreversibility, and time's arrow
// ----------------------------------------------------------------------------
// Engine-test recreation of engine-test-4.mp4. The subject is thermodynamics,
// but the vocabulary stays generic: persistent particles, authored layouts,
// plots, counters, arrows, LaTeX, and ordinary timeline composition.
// ============================================================================
title("Second Law of Thermodynamics");
canvas("9:16");
template("mono");
watermark(manicMark, (150, 145), "Made With Manic");
let cold = 194;
let pink = 326;
let hot = 40;
let blue = 226;
let green = 154;
let red = 10;
// ---------------------------------------------------------------------------
// 1 · MIXING MULTIPLIES THE NUMBER OF POSSIBLE MICROSTATES
// ---------------------------------------------------------------------------
equation(law, (540, 175), `S=k_B\ln\Omega`, 58);
rect(mixBox, (540, 520), 720, 280);
outline(mixBox, dim); outlined(mixBox); stroke(mixBox, 3);
rect(mixLeft, (360, 520), 350, 260); hidden(mixLeft);
rect(mixRight, (720, 520), 350, 260); hidden(mixRight);
line(mixDivider, (540, 390), (540, 650)); color(mixDivider, dim); stroke(mixDivider, 2);
particles(mixCold, mixLeft, 36, 6, 17);
particles(mixWarm, mixRight, 36, 6, 29);
hue(mixCold, cold, 0.72, 0.68); hue(mixWarm, pink, 0.72, 0.68);
glow(mixCold, 0.8); glow(mixWarm, 0.8);
text(mixCaption, (540, 745), "Mixing creates overwhelmingly more possible states");
size(mixCaption, 27); color(mixCaption, dim);
line(mixX, (260, 1040), (570, 1040)); line(mixY, (260, 1040), (260, 840));
color(mixX, dim); color(mixY, dim); stroke(mixX, 2); stroke(mixY, 2);
plot(mixCurve, (260, 1040), 78, 175, "1-exp(-2*x)", (0, 3.6));
hue(mixCurve, blue, 0.55, 0.68); stroke(mixCurve, 4); untraced(mixCurve);
dot(mixMarker, (260, 1040), 7); color(mixMarker, fg); glow(mixMarker, 1.0);
equation(mixDelta, (385, 815), `\Delta S/(Nk_B\ln 2)`, 27); color(mixDelta, dim);
counter(mixCount, (735, 945), 0, 1, "log₁₀(Ω/Ω₀) = ", "");
size(mixCount, 28); hue(mixCount, hot, 0.45, 0.72);
tag(mixBox, s1); tag(mixDivider, s1);
tag(mixCold, s1); tag(mixWarm, s1); tag(mixCaption, s1);
tag(mixX, s1); tag(mixY, s1); tag(mixCurve, s1); tag(mixMarker, s1); tag(mixDelta, s1); tag(mixCount, s1);
// ---------------------------------------------------------------------------
// 2 · HEAT FLOW MAKES TOTAL ENTROPY INCREASE
// ---------------------------------------------------------------------------
rect(hotBox, (315, 500), 260, 240); rect(coldBox, (765, 500), 260, 240);
outline(hotBox, dim); outline(coldBox, dim); outlined(hotBox); outlined(coldBox);
stroke(hotBox, 3); stroke(coldBox, 3);
particles(hotMatter, hotBox, 26, 6, 41); particles(coldMatter, coldBox, 26, 6, 73);
hue(hotMatter, hot, 0.66, 0.68); hue(coldMatter, blue, 0.66, 0.68);
link(heatLink, hotBox, coldBox); hue(heatLink, blue, 0.46, 0.62); stroke(heatLink, 4);
hidden(heatLink);
equation(hotLabel, (315, 325), `T_h`, 36); equation(coldLabel, (765, 325), `T_c`, 36);
hue(hotLabel, hot, 0.58, 0.72); hue(coldLabel, blue, 0.58, 0.72);
counter(hotTemp, (315, 680), 403, 0, "", " K");
counter(coldTemp, (765, 680), 397, 0, "", " K");
hue(hotTemp, hot, 0.55, 0.70); hue(coldTemp, blue, 0.55, 0.70);
text(flowCaption, (540, 780), "Heat flows until both sides reach one temperature");
size(flowCaption, 27); color(flowCaption, dim);
line(flowX, (305, 1130), (650, 1130)); line(flowY, (305, 1130), (305, 880));
color(flowX, dim); color(flowY, dim); stroke(flowX, 2); stroke(flowY, 2);
plot(flowCurve, (305, 1130), 78, 215, "1-exp(-2.4*x)", (0, 3.6));
hue(flowCurve, blue, 0.56, 0.70); stroke(flowCurve, 4); untraced(flowCurve);
dot(flowMarker, (305, 1130), 7); color(flowMarker, fg); glow(flowMarker, 1.0);
equation(flowS, (430, 855), `S_{\mathrm{tot}}`, 30); color(flowS, dim);
tag(hotBox, s2); tag(coldBox, s2); tag(hotMatter, s2); tag(coldMatter, s2);
tag(hotLabel, s2); tag(coldLabel, s2); tag(hotTemp, s2); tag(coldTemp, s2);
tag(flowCaption, s2); tag(flowX, s2); tag(flowY, s2); tag(flowCurve, s2); tag(flowMarker, s2); tag(flowS, s2);
hidden(s2);
// ---------------------------------------------------------------------------
// 3 · FREE EXPANSION: ONE GAS, TWICE THE VOLUME
// ---------------------------------------------------------------------------
rect(expandBox, (540, 520), 720, 280); outline(expandBox, dim); outlined(expandBox); stroke(expandBox, 3);
rect(expandLeft, (360, 520), 350, 260); hidden(expandLeft);
line(expandDivider, (540, 390), (540, 650)); color(expandDivider, dim); stroke(expandDivider, 2);
hidden(expandDivider);
particles(expandGas, expandLeft, 54, 6, 101);
hue(expandGas, cold, 0.38, 0.76); glow(expandGas, 0.7);
text(expandCaption, (540, 745), "More volume means exponentially more possible states");
size(expandCaption, 27); color(expandCaption, dim);
line(expandX, (250, 1040), (560, 1040)); line(expandY, (250, 1040), (250, 845));
color(expandX, dim); color(expandY, dim); stroke(expandX, 2); stroke(expandY, 2);
plot(expandCurve, (250, 1040), 85, 60, "x", (0, 2.6));
hue(expandCurve, green, 0.52, 0.70); stroke(expandCurve, 4); untraced(expandCurve);
dot(expandMarker, (250, 1040), 7); color(expandMarker, fg); glow(expandMarker, 1.0);
equation(expandAxis, (390, 820), `\ln(V/V_0)/\ln 2`, 27); color(expandAxis, dim);
equation(expandStates, (760, 905), `V^N\longrightarrow(2V)^N`, 37);
hue(expandStates, hot, 0.52, 0.70);
counter(expandCount, (760, 1010), 8.9, 1, "log₁₀(Ω/Ω₀) = ", "");
size(expandCount, 26); color(expandCount, dim);
tag(expandBox, s3); tag(expandGas, s3);
tag(expandCaption, s3); tag(expandX, s3); tag(expandY, s3); tag(expandCurve, s3);
tag(expandMarker, s3); tag(expandAxis, s3); tag(expandStates, s3); tag(expandCount, s3);
hidden(s3);
// ---------------------------------------------------------------------------
// 4 · A HEAT ENGINE CANNOT TURN EVERY JOULE INTO WORK
// ---------------------------------------------------------------------------
line(hotRail, (230, 410), (850, 410)); hue(hotRail, hot, 0.72, 0.65); stroke(hotRail, 5);
line(coldRail, (230, 1200), (850, 1200)); hue(coldRail, blue, 0.72, 0.65); stroke(coldRail, 5);
equation(hotRailLabel, (540, 365), `T_h`, 31); equation(coldRailLabel, (540, 1250), `T_c`, 31);
hue(hotRailLabel, hot, 0.56, 0.70); hue(coldRailLabel, blue, 0.56, 0.70);
line(engineX, (265, 1010), (805, 1010)); line(engineY, (265, 1010), (265, 555));
color(engineX, dim); color(engineY, dim); stroke(engineX, 2); stroke(engineY, 2);
equation(engineP, (250, 520), `p`, 28); equation(engineV, (835, 1010), `V`, 28);
color(engineP, dim); color(engineV, dim);
// Two paths share the hot state, separate during the cycle, and reconverge.
// Each visible path is split only to carry the warm/cool colour progression;
// the hidden whole paths give `travel` one continuous geometric route.
plot(engineHotPath, (285, 990), 112, 235, "1/(x+0.65)+0.14+0.05*x*(4.1-x)", (0, 4.1));
plot(engineColdPath, (285, 990), 112, 235, "1/(x+0.65)+0.14-0.035*x*(4.1-x)", (0, 4.1));
hidden(engineHotPath); hidden(engineColdPath);
plot(engineHotA, (285, 990), 112, 235, "1/(x+0.65)+0.14+0.05*x*(4.1-x)", (0, 1.55));
plot(engineHotB, (285, 990), 112, 235, "1/(x+0.65)+0.14+0.05*x*(4.1-x)", (1.55, 4.1));
plot(engineColdA, (285, 990), 112, 235, "1/(x+0.65)+0.14-0.035*x*(4.1-x)", (0, 1.85));
plot(engineColdB, (285, 990), 112, 235, "1/(x+0.65)+0.14-0.035*x*(4.1-x)", (1.85, 4.1));
hue(engineHotA, hot, 0.68, 0.70); hue(engineHotB, red, 0.58, 0.69);
hue(engineColdA, 260, 0.52, 0.72); hue(engineColdB, blue, 0.62, 0.70);
stroke(engineHotA, 5); stroke(engineHotB, 5); stroke(engineColdA, 5); stroke(engineColdB, 5);
untraced(engineHotA); untraced(engineHotB); untraced(engineColdA); untraced(engineColdB);
dot(engineDot, (285, 596), 8); color(engineDot, fg); glow(engineDot, 1.1); hidden(engineDot);
arrow(qHot, (330, 410), (330, 610)); hue(qHot, hot, 0.72, 0.68); stroke(qHot, 4); untraced(qHot);
equation(qHotLabel, (360, 525), `Q_h`, 28); hue(qHotLabel, hot, 0.56, 0.70); hidden(qHotLabel);
arrow(workArrow, (650, 850), (875, 700)); color(workArrow, dim); stroke(workArrow, 4); untraced(workArrow);
equation(workLabel, (880, 660), `W`, 28); color(workLabel, dim); hidden(workLabel);
arrow(qCold, (675, 1000), (675, 1170)); hue(qCold, blue, 0.72, 0.68); stroke(qCold, 4); untraced(qCold);
equation(qColdLabel, (735, 1090), `|Q_c|`, 28); hue(qColdLabel, blue, 0.56, 0.70); hidden(qColdLabel);
text(engineCaption, (540, 1345), "A heat engine must release some heat");
size(engineCaption, 30); hue(engineCaption, hot, 0.38, 0.74);
equation(engineEta, (540, 1490), `\eta_C=1-\frac{T_c}{T_h}<1`, 47);
// Persistent transition strokes preserve object identity between scenes:
// entropy curve → heat connector → expansion divider, then the volume graph
// seeds the heat-engine curve. All are ordinary generic `morph` paths.
plot(mixToHeat, (260, 1040), 78, 175, "1-exp(-2*x)", (0, 3.6));
hue(mixToHeat, blue, 0.55, 0.68); stroke(mixToHeat, 4);
morph(mixToHeat, heatLink); hidden(mixToHeat);
line(heatToExpand, (445, 500), (635, 500));
hue(heatToExpand, blue, 0.46, 0.62); stroke(heatToExpand, 4);
morph(heatToExpand, expandDivider); hidden(heatToExpand);
line(engineSeed, (285, 596), (355, 790)); hidden(engineSeed);
plot(expandToEngine, (250, 1040), 85, 60, "x", (0, 2.6));
hue(expandToEngine, green, 0.52, 0.70); stroke(expandToEngine, 4);
morph(expandToEngine, engineSeed); hidden(expandToEngine);
tag(hotRail, s4); tag(coldRail, s4); tag(hotRailLabel, s4); tag(coldRailLabel, s4);
tag(engineX, s4); tag(engineY, s4); tag(engineP, s4); tag(engineV, s4);
tag(engineHotA, s4); tag(engineHotB, s4); tag(engineColdA, s4); tag(engineColdB, s4);
tag(engineCaption, s4); tag(engineEta, s4);
tag(engineDot, s4future); tag(qHot, s4future); tag(qHotLabel, s4future);
tag(workArrow, s4future); tag(workLabel, s4future);
tag(qCold, s4future); tag(qColdLabel, s4future);
hidden(s4);
// ---------------------------------------------------------------------------
// 5 · SAME ENDPOINTS, DIFFERENT ENTROPY PRODUCTION
// ---------------------------------------------------------------------------
text(revHead, (300, 350), "REV. LIMIT"); text(irrHead, (780, 350), "IRREV");
size(revHead, 28); size(irrHead, 28); hue(revHead, green, 0.45, 0.72); hue(irrHead, red, 0.55, 0.68);
rect(revPanel, (300, 610), 330, 330); rect(irrPanel, (780, 610), 330, 330);
outline(revPanel, dim); outline(irrPanel, dim); outlined(revPanel); outlined(irrPanel);
stroke(revPanel, 3); stroke(irrPanel, 3);
rect(revGasBox, (260, 610), 220, 210); rect(irrGasBox, (740, 610), 220, 210);
hidden(revGasBox); hidden(irrGasBox);
rect(revSmall, (215, 610), 120, 210); rect(irrSmall, (695, 610), 120, 210);
hidden(revSmall); hidden(irrSmall);
particles(revGas, revGasBox, 20, 5, 131, "grid");
particles(irrGas, irrGasBox, 20, 5, 151, "grid");
hue(revGas, green, 0.55, 0.72); hue(irrGas, red, 0.58, 0.68);
line(revPiston, (405, 505), (405, 715)); line(irrPiston, (885, 505), (885, 715));
color(revPiston, dim); color(irrPiston, dim); stroke(revPiston, 5); stroke(irrPiston, 5);
text(compareCaption, (540, 865), "Same endpoints · different entropy production");
size(compareCaption, 28); color(compareCaption, dim);
rect(revMeter, (300, 1120), 70, 260); rect(irrMeter, (780, 1120), 70, 260);
outline(revMeter, dim); outline(irrMeter, dim); outlined(revMeter); outlined(irrMeter);
stroke(revMeter, 3); stroke(irrMeter, 3);
line(irrBar, (780, 1235), (780, 1235)); hue(irrBar, red, 0.72, 0.67); stroke(irrBar, 28);
line(revZero, (278, 1235), (322, 1235)); hue(revZero, green, 0.58, 0.70); stroke(revZero, 5);
equation(revEq, (300, 1325), `S_{\mathrm{gen}}=0`, 31);
equation(irrEq, (780, 1325), `S_{\mathrm{gen}}>0`, 31);
hue(revEq, green, 0.48, 0.70); hue(irrEq, red, 0.55, 0.68);
tag(revHead, s5); tag(irrHead, s5); tag(revPanel, s5); tag(irrPanel, s5);
tag(revGas, s5); tag(irrGas, s5); tag(revPiston, s5); tag(irrPiston, s5); tag(compareCaption, s5);
tag(revMeter, s5); tag(irrMeter, s5); tag(irrBar, s5); tag(revZero, s5); tag(revEq, s5); tag(irrEq, s5);
hidden(s5);
// ---------------------------------------------------------------------------
// 6 · THE ARROW OF TIME IS A STATISTICAL ARROW
// ---------------------------------------------------------------------------
rect(timeBox, (540, 610), 720, 310); outline(timeBox, dim); outlined(timeBox); stroke(timeBox, 3);
particles(timeGas, timeBox, 63, 6, 211, "grid");
hue(timeGas, hot, 0.48, 0.74); glow(timeGas, 0.7);
arrow(timeForward, (300, 980), (780, 980)); hue(timeForward, red, 0.62, 0.69); stroke(timeForward, 5);
equation(timeForwardLabel, (540, 1035), `t`, 28); color(timeForwardLabel, dim);
arrow(timeReverse, (780, 980), (300, 980)); color(timeReverse, fg); stroke(timeReverse, 5); hidden(timeReverse);
equation(timeReverseLabel, (540, 1035), `t`, 28); color(timeReverseLabel, dim); hidden(timeReverseLabel);
text(timeCaption, (540, 1135), "A special state naturally spreads into a typical one");
size(timeCaption, 29); hue(timeCaption, hot, 0.34, 0.74);
equation(returnProb, (540, 285), `\Pr(\mathrm{return})=\frac{\Omega_{\mathrm{low}}}{\Omega_{\mathrm{eq}}}=e^{-\Delta S/k_B}`, 43);
equation(returnOdds, (540, 365), `\Pr(\mathrm{ordered\ return})<10^{-40}`, 34);
hue(returnOdds, hot, 0.48, 0.72); hidden(returnProb); hidden(returnOdds);
tag(timeBox, s6); tag(timeGas, s6gas); tag(timeForward, s6); tag(timeForwardLabel, s6);
tag(timeCaption, s6);
tag(timeReverse, s6future); tag(timeReverseLabel, s6future);
tag(returnProb, s6future); tag(returnOdds, s6future);
hidden(s6); hidden(s6gas);
circle(finalOrbit, (540, 790), 270); hidden(finalOrbit);
for i in 0..72 {
let a = tau*i/72;
dot(finalDot{i}, (540 + 270*cos(a), 790 + 270*sin(a)), 4);
hue(finalDot{i}, 10 + 300*i/72, 0.54, 0.62);
glow(finalDot{i}, 0.5);
tag(finalDot{i}, finalRing);
}
equation(finalLaw, (540, 765), `\Delta S_{\mathrm{universe}}=S_{\mathrm{gen}}\ge 0`, 48);
text(finalCaption, (540, 875), "Irreversible change produces entropy");
size(finalCaption, 31); color(finalCaption, dim);
hidden(finalRing); hidden(finalLaw); hidden(finalCaption);
// ---------------------------------------------------------------------------
// TIMELINE · six continuous beats, approximately 39 seconds
// ---------------------------------------------------------------------------
step("mixing") {
wait(5.5);
par {
seq { wait(0.45); fade(mixDivider, 0.20); }
seq { wait(0.55); arrange(mixCold, mixBox, "random", 0.95, out); wander(mixCold, 4.0); }
seq { wait(0.55); arrange(mixWarm, mixBox, "random", 0.95, out); wander(mixWarm, 4.0); }
seq {
wait(0.55);
par {
draw(mixCurve, 2.20, out);
travel(mixMarker, mixCurve, 2.20, out);
to(mixCount, value, 25.3, 2.20, out);
}
}
}
}
step("heat-flow") {
seq {
par {
fade(s1, 0.65);
show(mixToHeat, 0.10);
to(mixToHeat, morph, 1, 0.65, smooth);
seq { wait(0.20); show(s2, 0.45); }
rewrite(law, `\dot S_{\mathrm{tot}}=K\frac{(T_h-T_c)^2}{T_hT_c}>0`, 0.65, smooth);
}
par {
wander(hotMatter, 4.4);
wander(coldMatter, 4.4);
draw(flowCurve, 3.6, smooth);
travel(flowMarker, flowCurve, 3.6, smooth);
to(hotTemp, value, 400, 4.0, smooth);
to(coldTemp, value, 400, 4.0, smooth);
recolor(hotMatter, cyan, 4.0);
recolor(coldMatter, cyan, 4.0);
seq { flow(mixToHeat, 1.1); flow(mixToHeat, 1.1); flow(mixToHeat, 1.1); flow(mixToHeat, 1.1); }
}
wait(0.45);
}
}
step("free-expansion") {
seq {
par {
fade(s2, 0.65);
fade(mixToHeat, 0.25);
show(heatToExpand, 0.10);
to(heatToExpand, morph, 1, 0.65, smooth);
recolor(heatToExpand, dim, 0.65);
seq { wait(0.20); show(s3, 0.45); }
rewrite(law, `\Delta S=Nk_B\ln 2`, 0.65, smooth);
}
par {
seq { arrange(expandGas, expandBox, "random", 1.10, out); wander(expandGas, 1.45); }
fade(heatToExpand, 0.65);
draw(expandCurve, 2.45, smooth);
travel(expandMarker, expandCurve, 2.45, smooth);
to(expandCount, value, 20.9, 2.45, smooth);
}
wait(0.25);
}
}
step("heat-engine") {
seq {
par {
fade(s3, 0.65);
show(expandToEngine, 0.10);
to(expandToEngine, morph, 1, 0.65, smooth);
seq { wait(0.20); show(s4, 0.45); }
rewrite(law, `\frac{Q_h}{T_h}=\frac{|Q_c|}{T_c}`, 0.65, smooth);
}
par {
seq { draw(engineHotA, 1.85, smooth); draw(engineHotB, 3.35, smooth); }
seq { draw(engineColdA, 2.35, smooth); draw(engineColdB, 2.85, smooth); }
seq {
wait(0.15); show(engineDot, 0.15);
travel(engineDot, engineColdPath, 4.9, smooth);
}
fade(expandToEngine, 0.30);
seq { draw(qHot, 0.7); show(qHotLabel, 0.2); wait(1.0); }
seq { wait(1.3); draw(workArrow, 0.8); show(workLabel, 0.2); }
seq { wait(2.6); draw(qCold, 0.8); show(qColdLabel, 0.2); }
}
wait(2.15);
}
}
step("entropy-generation") {
seq {
par {
fade(s4, 0.50); fade(s4future, 0.50);
show(s5, 0.50);
rewrite(law, `\Delta S_{\mathrm{universe}}=S_{\mathrm{gen}}`, 0.50, smooth);
}
par {
arrange(revGas, revSmall, "grid", 2.4, smooth);
arrange(irrGas, irrSmall, "random", 2.4, smooth);
par { move(revPiston, (275, 505), 2.4, smooth); grow(revPiston, (275, 715), 2.4, smooth); }
par { move(irrPiston, (755, 505), 2.4, smooth); grow(irrPiston, (755, 715), 2.4, smooth); }
grow(irrBar, (780, 1025), 2.4, smooth);
}
wait(1.10);
}
}
step("time-arrow") {
seq {
par {
fade(s5, 0.50); fade(law, 0.50);
show(s6, 0.50); show(s6gas, 0.50);
}
par {
arrange(timeGas, timeBox, "random", 4.0, smooth);
seq { wait(3.0); pulse(timeForward, 0.8); }
}
wait(0.35);
par {
fade(timeForward, 0.30); fade(timeForwardLabel, 0.30);
show(timeReverse, 0.30); show(timeReverseLabel, 0.30);
say(timeCaption, "Exact reversal can reconstruct the past", 0.35);
}
arrange(timeGas, timeBox, "grid", 3.1, smooth);
par {
show(returnProb, 0.45); show(returnOdds, 0.45);
say(timeCaption, "But that reversal must be fantastically precise", 0.40);
}
wait(1.15);
par {
fade(s6, 0.90);
fade(s6future, 0.90);
arrange(timeGas, finalOrbit, "ring", 0.90, smooth);
seq { wait(0.55); fade(s6gas, 0.35); }
seq { wait(0.55); show(finalRing, 0.35); }
seq { wait(0.55); transform(finalRing, (540,790), 0.9511, -0.3090, 0.3090, 0.9511, 0.55, out); }
seq { wait(0.30); par { show(finalLaw, 0.60); show(finalCaption, 0.60); } }
}
wait(1.70);
}
}
timing-v2-scene
Generic Timing v2 controlling an ordinary physics scene: one named-phase clock schedules the intro, pendulum motion and finish independently from its native timer look.
// Generic Timing v2 — one named clock coordinates a non-quiz physics scene.
// `timed` runs the visual timer and places every `during` block at the exact
// offset declared by `timing`; short blocks are padded, overruns are errors.
title("Generic Timing v2 — Pendulum");
canvas("16:9");
template("mono");
watermark(manicMark, (w*0.955-100, h*0.045+24), "Made With Manic");
text(head, (315, 72), "ONE CLOCK. EVERY BEAT.");
size(head, 30); bold(head); color(head, fg); hidden(head);
text(sub, (315, 112), "Generic Timing v2 — not a quiz");
size(sub, 18); color(sub, dim); hidden(sub);
pendulum(p, (640, 245), 1.5, 42, 140, 0.03);
untraced(p.path);
equation(law, (640, 602), `T\approx2\pi\sqrt{L/g}`, 54);
color(law, gold); hidden(law);
text(note, (640, 655), "The scene and clock share the same named phases.");
size(note, 21); color(note, dim); hidden(note);
text(done, (640, 680), "INTRO → EXPERIMENT → INSIGHT → OUTRO");
size(done, 18); color(done, cyan); hidden(done);
// A fresh id creates a format-neutral timing controller. Its phases total
// 10.4 seconds; no duplicate duration is passed to `timed` or `run`.
timing(showclock, (970, 86), "intro=1.2 experiment=6 insight=2 outro=1.2");
timerstyle(showclock,
"look=segments number=outside direction=drain size=small thickness=1.15 color=cyan track=dim label=MASTER_CLOCK font=display finish=pulse");
timed(showclock) {
during("intro") {
par { show(head, 0.7); show(sub, 0.7); }
}
during("experiment") {
par { run(p, 6); draw(p.path, 6); }
}
during("insight") {
par { show(law, 0.6); show(note, 0.6); }
pulse(p.bob, 0.7);
}
during("outro") {
show(done, 0.6);
}
}
zeroth-law-thermodynamics
The Zeroth Law told through three particle-filled bodies: thermal relations connect, the bodies settle onto one temperature axis, and equilibrium becomes visible.
// ============================================================================
// zeroth-law-thermodynamics.manic — thermal equalisation explained visually
// ----------------------------------------------------------------------------
// A shot-for-shot structural recreation of engine-test.mp4 using only the
// generic std + math vocabulary. It deliberately avoids a thermal-specific
// kit: circles, dots, lines, LaTeX, formula plots and timeline composition
// should be enough to tell the story.
//
// Reference beats: network 0–9s · temperature axis 9–20s · transfer 20–35s
// · closing thought 35–39.4s. Record at 60 fps with the reel preset.
// ============================================================================
title("Zeroth Law of Thermodynamics");
canvas("9:16");
template("mono");
// Reels put controls down the right edge and captions near the bottom. Keep
// identity in the upper-left safe area, clear of both UI and the three bodies.
watermark(manicMark, (155, 145), "Made With Manic");
// Shared palette expressed as hues so the example keeps its identity even
// when somebody overrides the template from the CLI.
let teal = 168;
let warm = 38;
let cool = 246;
// ---------------------------------------------------------------------------
// SCENE 1 · three bodies connected by possible heat exchange
// ---------------------------------------------------------------------------
circle(s1A, (540, 280), 92);
circle(s1B, (300, 565), 92);
circle(s1C, (780, 565), 92);
outline(s1A, cyan); outline(s1B, cyan); outline(s1C, cyan);
stroke(s1A, 5); stroke(s1B, 5); stroke(s1C, 5);
glow(s1A, 1.5); glow(s1B, 1.5); glow(s1C, 1.5);
tag(s1A, s1); tag(s1B, s1); tag(s1C, s1);
equation(s1la, (540, 280), `A`, 68);
equation(s1lb, (300, 565), `B`, 68);
equation(s1lc, (780, 565), `C`, 68);
tag(s1la, s1); tag(s1lb, s1); tag(s1lc, s1);
// Generic contained dots: the ids make these "matter" here, but the engine
// words are equally useful for bubbles, dust, stars, or moving data.
particles(s1matterA, s1A, 22, 5, 7);
particles(s1matterB, s1B, 22, 5, 17);
particles(s1matterC, s1C, 22, 5, 27);
hue(s1matterA, teal, 0.65, 0.70); hue(s1matterB, teal, 0.65, 0.70); hue(s1matterC, teal, 0.65, 0.70);
glow(s1matterA, 1.0); glow(s1matterB, 1.0); glow(s1matterC, 1.0);
tag(s1matterA, s1); tag(s1matterB, s1); tag(s1matterC, s1);
// Curved tracked links replace hand-computed endpoints and keep following if
// the bodies move. Signed bends bow the two upper links away from the triangle.
link(s1ab, s1A, s1B, 48);
link(s1ac, s1A, s1C, -48);
link(s1bc, s1B, s1C, 55);
stroke(s1ab, 4); stroke(s1ac, 4); stroke(s1bc, 4);
hue(s1ab, teal, 0.72, 0.54); hue(s1ac, teal, 0.72, 0.54); hue(s1bc, teal, 0.72, 0.54);
glow(s1ab, 0.8); glow(s1ac, 0.8); glow(s1bc, 0.8);
untraced(s1ab); untraced(s1ac); untraced(s1bc);
tag(s1ab, s1); tag(s1ac, s1); tag(s1bc, s1); tag(s1ab, s1links); tag(s1ac, s1links); tag(s1bc, s1links);
// Each relation first appears as a faint possibility, then becomes definite as
// its curved connection draws on.
equation(s1eqAB, (315, 365), `A\sim B`, 35);
equation(s1eqBC, (540, 660), `B\sim C`, 35);
equation(s1eqAC, (765, 365), `A\sim C`, 35);
color(s1eqAB, dim); color(s1eqBC, dim); color(s1eqAC, dim);
tag(s1eqAB, s1); tag(s1eqBC, s1); tag(s1eqAC, s1);
tag(s1eqAB, s1relations); tag(s1eqBC, s1relations); tag(s1eqAC, s1relations);
tag(s1A, s1bodyA); tag(s1la, s1bodyA); tag(s1matterA, s1bodyA);
tag(s1B, s1bodyB); tag(s1lb, s1bodyB); tag(s1matterB, s1bodyB);
tag(s1C, s1bodyC); tag(s1lc, s1bodyC); tag(s1matterC, s1bodyC);
hidden(s1A); hidden(s1B); hidden(s1C);
hidden(s1la); hidden(s1lb); hidden(s1lc);
hidden(s1matterA); hidden(s1matterB); hidden(s1matterC);
hidden(s1eqAB); hidden(s1eqBC); hidden(s1eqAC);
// ---------------------------------------------------------------------------
// SCENE 2 · temperature becomes a position on one common scale
// ---------------------------------------------------------------------------
line(s2y, (180, 700), (180, 1390));
line(s2g0, (180, 820), (900, 820));
line(s2g1, (180, 1070), (900, 1070));
line(s2g2, (180, 1320), (900, 1320));
line(s2eqline, (230, 1070), (850, 1070));
color(s2y, blue); color(s2g0, dim); color(s2g1, dim); color(s2g2, dim); hue(s2eqline, teal, 0.65, 0.55);
stroke(s2y, 4); stroke(s2g0, 2); stroke(s2g1, 2); stroke(s2g2, 2); stroke(s2eqline, 5);
glow(s2eqline, 0.9);
tag(s2y, s2); tag(s2g0, s2); tag(s2g1, s2); tag(s2g2, s2); tag(s2eqline, s2);
tag(s2y, s2axes); tag(s2g0, s2axes); tag(s2g1, s2axes); tag(s2g2, s2axes); tag(s2eqline, s2axes);
equation(s2T, (180, 650), `T`, 44); color(s2T, fg); tag(s2T, s2); tag(s2T, s2axes);
dot(s2A, (430, 1070), 10); dot(s2B, (540, 1070), 10); dot(s2C, (650, 1070), 10);
hue(s2A, teal, 0.65, 0.60); hue(s2B, teal, 0.65, 0.60); hue(s2C, teal, 0.65, 0.60);
glow(s2A, 1.1); glow(s2B, 1.1); glow(s2C, 1.1);
tag(s2A, s2); tag(s2B, s2); tag(s2C, s2); tag(s2A, s2points); tag(s2B, s2points); tag(s2C, s2points);
equation(s2la, (430, 1025), `A`, 35); equation(s2lb, (540, 1025), `B`, 35); equation(s2lc, (650, 1025), `C`, 35);
tag(s2la, s2); tag(s2lb, s2); tag(s2lc, s2); tag(s2la, s2points); tag(s2lb, s2points); tag(s2lc, s2points);
equation(s2eq, (540, 1490), `T_A=T_B=T_C`, 48); tag(s2eq, s2); tag(s2eq, s2eqgroup);
line(s2hot, (230, 820), (850, 820)); line(s2cold, (230, 1320), (850, 1320));
hue(s2hot, warm, 0.70, 0.62); hue(s2cold, cool, 0.70, 0.62); stroke(s2hot, 4); stroke(s2cold, 4);
line(s2bracket, (850, 820), (850, 1320)); line(s2cap0, (825, 820), (875, 820)); line(s2cap1, (825, 1320), (875, 1320));
color(s2bracket, fg); color(s2cap0, fg); color(s2cap1, fg); stroke(s2bracket, 3); stroke(s2cap0, 3); stroke(s2cap1, 3);
equation(s2delta, (930, 1070), `\Delta T`, 42);
tag(s2hot, s2deltaGroup); tag(s2cold, s2deltaGroup); tag(s2bracket, s2deltaGroup);
tag(s2cap0, s2deltaGroup); tag(s2cap1, s2deltaGroup); tag(s2delta, s2deltaGroup);
tag(s2hot, s2); tag(s2cold, s2); tag(s2bracket, s2); tag(s2cap0, s2); tag(s2cap1, s2); tag(s2delta, s2);
hidden(s2);
// ---------------------------------------------------------------------------
// SCENE 3 · energy flows while two temperatures converge exponentially
// ---------------------------------------------------------------------------
circle(s3A, (300, 430), 98); circle(s3C, (780, 430), 98);
outline(s3A, orange); outline(s3C, blue);
stroke(s3A, 5); stroke(s3C, 5); glow(s3A, 1.5); glow(s3C, 1.5);
tag(s3A, s3initialrings); tag(s3C, s3initialrings);
link(s3link, s3A, s3C); hue(s3link, teal, 0.65, 0.54); stroke(s3link, 5); glow(s3link, 0.8);
// Crossfade to common teal rings as equilibrium approaches. Keeping the fill
// dark preserves the reservoir/particle texture instead of becoming a disc.
circle(s3Afinal, (300, 430), 98); circle(s3Cfinal, (780, 430), 98);
outline(s3Afinal, cyan); outline(s3Cfinal, cyan); stroke(s3Afinal, 5); stroke(s3Cfinal, 5);
glow(s3Afinal, 1.5); glow(s3Cfinal, 1.5); hidden(s3Afinal); hidden(s3Cfinal);
tag(s3Afinal, s3); tag(s3Cfinal, s3); tag(s3Afinal, s3finalrings); tag(s3Cfinal, s3finalrings);
equation(s3la, (300, 430), `A`, 70); equation(s3lc, (780, 430), `C`, 70);
tag(s3link, s3nodes); tag(s3A, s3nodes); tag(s3C, s3nodes); tag(s3la, s3nodes); tag(s3lc, s3nodes);
tag(s3link, s3); tag(s3A, s3); tag(s3C, s3); tag(s3la, s3); tag(s3lc, s3);
particles(s3hotparticles, s3A, 24, 5, 41);
particles(s3coldparticles, s3C, 24, 5, 73);
hue(s3hotparticles, warm, 0.72, 0.68); hue(s3coldparticles, cool, 0.72, 0.68);
glow(s3hotparticles, 1.0); glow(s3coldparticles, 1.0);
tag(s3hotparticles, s3); tag(s3hotparticles, s3nodes);
tag(s3coldparticles, s3); tag(s3coldparticles, s3nodes);
// Temperature-vs-time graph. The two exact formulas share the same asymptote.
line(s3x, (190, 1340), (900, 1340)); line(s3y, (190, 1340), (190, 760));
color(s3x, dim); color(s3y, dim); stroke(s3x, 3); stroke(s3y, 3);
equation(s3theta, (190, 710), `\theta`, 40); equation(s3time, (930, 1340), `t`, 38);
plot(s3upper, (230, 1340), 155, 190, "1 + exp(-x)", (0, 4));
plot(s3lower, (230, 1340), 155, 190, "1 - exp(-x)", (0, 4));
hue(s3upper, warm, 0.78, 0.62); hue(s3lower, cool, 0.78, 0.62); stroke(s3upper, 5); stroke(s3lower, 5);
glow(s3upper, 0.7); glow(s3lower, 0.7); untraced(s3upper); untraced(s3lower);
tag(s3x, s3); tag(s3y, s3); tag(s3theta, s3); tag(s3time, s3); tag(s3upper, s3); tag(s3lower, s3);
tag(s3x, s3axes); tag(s3y, s3axes); tag(s3theta, s3axes); tag(s3time, s3axes);
tag(s3upper, s3curves); tag(s3lower, s3curves);
equation(s3eq, (540, 1510), `\Delta T(t)=\Delta T_0 e^{-2k t/C}`, 44);
color(s3eq, fg); tag(s3eq, s3); tag(s3eq, s3equation);
hidden(s3);
// ---------------------------------------------------------------------------
// SCENE 4 · reduce the mechanics to one thought
// ---------------------------------------------------------------------------
line(s4line, (420, 850), (660, 850)); hue(s4line, teal, 0.72, 0.58); stroke(s4line, 6); glow(s4line, 1.0);
dot(s4a, (420, 850), 16); dot(s4c, (660, 850), 16); hue(s4a, teal, 0.70, 0.63); hue(s4c, teal, 0.70, 0.63); glow(s4a, 1.3); glow(s4c, 1.3);
text(s4quote, (540, 1130), "Temperature names sameness.\nDifference gives energy direction.");
size(s4quote, 38); color(s4quote, fg); bold(s4quote); untraced(s4quote);
tag(s4line, s4); tag(s4a, s4); tag(s4c, s4); tag(s4quote, s4);
hidden(s4line); hidden(s4a); hidden(s4c);
// ---------------------------------------------------------------------------
// TIMELINE · 39.4 seconds
// ---------------------------------------------------------------------------
par {
wander(s1matterA, 7.5);
wander(s1matterB, 7.5);
wander(s1matterC, 7.5);
seq {
show(s1bodyA, 0.45);
wait(0.20);
show(s1bodyB, 0.45);
show(s1eqAB, 0.25);
par { draw(s1ab, 0.70); recolor(s1eqAB, fg, 0.70); }
flow(s1ab, 0.75);
show(s1bodyC, 0.45);
show(s1eqBC, 0.25);
par { draw(s1bc, 0.70); recolor(s1eqBC, fg, 0.70); }
flow(s1bc, 0.75);
show(s1eqAC, 0.25);
par { draw(s1ac, 0.70); recolor(s1eqAC, fg, 0.70); }
par { flow(s1ab, 1.05); flow(s1bc, 1.05); flow(s1ac, 1.05); }
wait(0.55);
}
}
// The three bodies do not cut to new dots: they visibly shrink, line up on a
// common temperature axis, then hand off to the precise plotted markers.
par {
fade(s1relations, 0.45); fade(s1links, 0.45);
fade(s1matterA, 0.45); fade(s1matterB, 0.45); fade(s1matterC, 0.45);
show(s2axes, 0.70);
move(s1A, (430, 1070), 1.10, smooth); scale(s1A, 0.12, 1.10, smooth);
move(s1B, (540, 1070), 1.10, smooth); scale(s1B, 0.12, 1.10, smooth);
move(s1C, (650, 1070), 1.10, smooth); scale(s1C, 0.12, 1.10, smooth);
move(s1la, (430, 1025), 1.10, smooth); scale(s1la, 0.52, 1.10, smooth);
move(s1lb, (540, 1025), 1.10, smooth); scale(s1lb, 0.52, 1.10, smooth);
move(s1lc, (650, 1025), 1.10, smooth); scale(s1lc, 0.52, 1.10, smooth);
}
par {
fade(s1bodyA, 0.22); fade(s1bodyB, 0.22); fade(s1bodyC, 0.22);
show(s2points, 0.22);
}
show(s2eqgroup, 0.5);
wait(4.0);
par {
move(s2A, (430, 820), 1.3, smooth); move(s2la, (430, 775), 1.3, smooth);
move(s2C, (650, 1320), 1.3, smooth); move(s2lc, (650, 1275), 1.3, smooth);
recolor(s2A, orange, 1.0); recolor(s2la, orange, 1.0);
recolor(s2C, blue, 1.0); recolor(s2lc, blue, 1.0);
fade(s2eqgroup, 0.5);
}
show(s2deltaGroup, 0.5);
wait(3.5);
// Keep visual identity across the scene change: the two temperature points
// first become small bodies on their own level lines, then travel and enlarge
// into the particle-filled reservoirs. The old axis remains behind them until
// the new transfer graph is already visible—there is never an empty frame.
par {
fade(s2B, 0.40); fade(s2lb, 0.40);
scale(s2A, 5.0, 0.70, smooth); move(s2la, (430, 820), 0.70, smooth); scale(s2la, 1.35, 0.70, smooth);
scale(s2C, 5.0, 0.70, smooth); move(s2lc, (650, 1320), 0.70, smooth); scale(s2lc, 1.35, 0.70, smooth);
}
par {
move(s2A, (300, 430), 0.50, smooth); scale(s2A, 9.8, 0.50, smooth);
move(s2la, (300, 430), 0.50, smooth); scale(s2la, 2.0, 0.50, smooth);
move(s2C, (780, 430), 0.50, smooth); scale(s2C, 9.8, 0.50, smooth);
move(s2lc, (780, 430), 0.50, smooth); scale(s2lc, 2.0, 0.50, smooth);
show(s3axes, 0.50);
}
par {
show(s3nodes, 0.70);
fade(s2, 0.70);
}
show(s3curves, 0.01);
par {
draw(s3upper, 10.7, smooth);
draw(s3lower, 10.7, smooth);
wander(s3hotparticles, 10.7);
wander(s3coldparticles, 10.7);
fade(s3initialrings, 10.5); show(s3finalrings, 10.5);
recolor(s3hotparticles, cyan, 10.5); recolor(s3coldparticles, cyan, 10.5);
seq {
for i in 0..6 { seq { flow(s3link, 1.20); wait(0.55); } }
}
}
show(s3equation, 0.6);
wait(1.5);
fade(s3, 0.8);
show(s4line, 0.5); show(s4a, 0.01); show(s4c, 0.01);
type(s4quote, 1.2);
wait(2.55); // keeps the benchmark at 39.4 s including manic's final 1 s tail
pendulum-damped
The same four views with friction on (damping): the swing decays, the phase loop
spirals inward, the well ball settles, and the total-energy line drops — dissipation
told the same way by every panel.
// ============================================================================
// pendulum-damped.manic — the SAME four views, now with friction
// ----------------------------------------------------------------------------
// Identical to `pendulum.manic` but with `damping` (the 6th arg) turned on, so
// the swing loses energy. Watch every panel respond together:
// · sim — the amplitude shrinks each swing (slows toward rest)
// · phase — the closed loop becomes an INWARD SPIRAL
// · well — the ball SETTLES toward the bottom of the bowl
// · energygraph — the TOTAL (gold) line DECAYS (KE↔PE trade, sum drops)
//
// Set damping = 0 (or drop the 6th arg) for the frictionless version, where the
// loop stays closed and the total-energy line stays flat (conservation).
// ============================================================================
title("Damped pendulum — energy bleeds away");
canvas("16:9");
text(hdr, (cx, 40), "Add friction: the swing decays, and every view follows");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the physical sim (left) — 6th arg 0.6 is the damping
pendulum(p, (250, 220), 1.2, 55, 105, 0.6);
untraced(p.path);
// the same four math views of the SAME (now damped) simulation
phase(p, (715, 165), 90); // loop → inward spiral
timegraph(p, (1000, 165), 90); // θ(t), ω(t) — envelope shrinks
well(p, (715, 455), 90); // ball settles to the bottom
energygraph(p, (1000, 455), 90); // total energy decays
text(cap, (cx, h - 30), "damping = 0.6 · the phase spiral, settling ball, and decaying total energy all show the same loss");
size(cap, 16); color(cap, dim); display(cap);
draw(p.path, 0.8);
swing(p, 10);
pendulum-annotated
A guided anatomy lesson proving physics composes with base manic: section chapters,
text / arrow / bracelabel annotations, and show/recolor/flash/pulse all
driving the sim’s parts — no special physics mode.
// ============================================================================
// pendulum-annotated.manic — a guided lesson: physics + base manic compose
// ----------------------------------------------------------------------------
// Proof that a sim's parts (`{id}.pivot/.rod/.bob/.path/…`) are ORDINARY manic
// entities. This whole lesson is built from BASE std vocabulary — `section`
// chapters, `text` labels, leader `arrow`s, a reference `line`, a `bracelabel`,
// and `show`/`fade`/`recolor`/`flash`/`pulse`/`draw`/`say` — wrapped around the
// physics `pendulum` + `energygraph` view + `swing`. No special "physics mode":
// every std verb/modifier/annotation addresses the physics entities directly.
// ============================================================================
title("Anatomy of a Pendulum");
canvas("16:9");
text(hdr, (cx, 44), "Anatomy of a Pendulum");
size(hdr, 30); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// ---- the physics sim + one math view (parts start hidden; path untraced) ----
pendulum(p, (440, 210), 1.6, 50, 110);
hidden(p.pivot); hidden(p.rod); hidden(p.bob); hidden(p.overlays);
untraced(p.path); // trace 0 but keep opacity (reveal later)
energygraph(p, (1030, 250), 105); // built now (its sweep joins `swing`)
hidden(p.energy);
// ---- base annotations, all hidden to reveal in order ----
line(vref, (440, 210), (440, 386)); // vertical reference
color(vref, dim); stroke(vref, 2); hidden(vref);
bracelabel(Lb, (440, 210), (575, 323), "L = 1.6 m", 26); // rod length
color(Lb, gold); hidden(Lb); hidden(Lb.label);
text(pivL, (330, 200), "pivot (fixed)"); size(pivL, 18); color(pivL, dim); display(pivL); hidden(pivL);
arrow(pivA, (388, 205), (428, 209)); color(pivA, dim); stroke(pivA, 2); hidden(pivA);
text(bobL, (600, 355), "bob — mass m"); size(bobL, 18); color(bobL, magenta); display(bobL); hidden(bobL);
arrow(bobA, (628, 345), (585, 330)); color(bobA, dim); stroke(bobA, 2); hidden(bobA);
text(angL, (472, 300), "θ₀ = 50°"); size(angL, 18); color(angL, lime); display(angL); hidden(angL);
text(cap, (cx, h - 42), ""); size(cap, 20); color(cap, dim); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
section("Anatomy");
say(cap, "a fixed pivot …", 0.4);
show(p.pivot, 0.4); pulse(p.pivot); show(pivL, 0.4); draw(pivA, 0.3);
wait(0.5);
say(cap, "… a rigid rod of length L …", 0.4);
show(p.rod, 0.4); show(Lb, 0.4); show(Lb.label, 0.4);
wait(0.6);
say(cap, "… and a bob of mass m at the end", 0.4);
show(p.bob, 0.5); recolor(p.bob, magenta, 0.3); pulse(p.bob); show(bobL, 0.4); draw(bobA, 0.3);
wait(0.5);
say(cap, "all one group — a base broadcast flashes every part at once", 0.4);
flash(p.parts, lime); // std broadcast over the whole sim
wait(0.7);
section("Release");
say(cap, "held at θ₀ from the vertical, then let go", 0.4);
show(vref, 0.4); show(angL, 0.4); flash(p.rod, cyan);
wait(0.9);
// clear the static annotations before the motion
fade(Lb, 0.3); fade(Lb.label, 0.3); fade(pivA, 0.3); fade(bobA, 0.3);
fade(pivL, 0.3); fade(bobL, 0.3); fade(angL, 0.3); fade(vref, 0.3);
wait(0.3);
section("Swing");
say(cap, "gravity pulls it back — velocity arrow + energy bars ride along", 0.4);
show(p.overlays, 0.4); // reveal the velocity arrow + KE/PE bars
draw(p.path, 0.8); // trace the arc it will follow
section("Energy");
say(cap, "and the SAME swing, read as energy over time", 0.4);
show(p.energy, 0.5); // reveal the energy graph, then run it all
swing(p, 10); // every panel + annotation-free scene animates
spring
A mass on a spring (simple harmonic motion) drawn with a real stretching coil — the
same generic views on a different system; note the energy well is a parabola
(½kx²) rather than the pendulum’s cosine (spring + the views + run).
// ============================================================================
// spring.manic — the physics kit's SECOND sim (Layer 1)
// ----------------------------------------------------------------------------
// `spring(id, [center], [stiffness], [x0], [unit], [damping])` — a mass on a
// spring, PRE-SIMULATED with RK4 (deterministic). Only `id` is required. It's a
// different system from the pendulum, yet it inherits the SAME generic views for
// free — note the energy well here is a PARABOLA U(x)=½kx² (the pendulum's is a
// cosine). `run(id, [dur])` replays the motion (alias: `swing`).
//
// TO ADAPT: add damping (6th arg) — the phase ellipse spirals in, the ball
// settles to the bottom of the parabola, and total energy decays:
// `spring(s, (360,320), 10, 1.4, 110, 0.6)`.
// ============================================================================
title("Mass on a spring — four views");
canvas("16:9");
text(hdr, (cx, 44), "A different sim — same four views (well is a parabola)");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the mass–spring on the left
spring(s, (330, 300), 10, 1.4, 110);
// the same generic views of the SAME simulation, 2×2 on the right
phase(s, (760, 175), 90); // x vs v — an ellipse
timegraph(s, (1010, 175), 90); // x(t), v(t) with a sweep line
well(s, (760, 460), 90); // parabolic energy well U(x)=½kx²
energygraph(s, (1010, 460), 90); // KE / PE / total over time
text(cap, (cx, h - 32), "one mass–spring, seen as motion · phase ellipse · time series · parabolic well · energy");
size(cap, 16); color(cap, dim); display(cap);
run(s, 10); // every panel animates in lockstep
spring-damped
The damped spring: the coil’s oscillation decays, the phase ellipse spirals in, the ball settles in the parabola, and total energy bleeds away.
// ============================================================================
// spring-damped.manic — the same four views, now with friction
// ----------------------------------------------------------------------------
// Identical to `spring.manic` but with `damping` (the 6th arg) turned on, so the
// oscillation loses energy. Watch every panel respond together:
// · sim — the coil's swing SHRINKS each cycle (settles to rest)
// · phase — the ellipse becomes an INWARD SPIRAL
// · well — the ball SETTLES to the bottom of the parabola U(x)=½kx²
// · energygraph — the TOTAL (gold) line DECAYS (KE↔PE trade, sum drops)
//
// Set damping = 0 (or drop the 6th arg) for the frictionless version, where the
// ellipse stays closed and the total-energy line stays flat (conservation).
// ============================================================================
title("Damped spring — energy bleeds away");
canvas("16:9");
text(hdr, (cx, 44), "Add friction: the oscillation decays, every view follows");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the mass–spring (left) — 6th arg 0.6 is the damping
spring(s, (330, 300), 10, 1.4, 110, 0.6);
// the same generic views of the SAME (now damped) simulation
phase(s, (760, 175), 90); // ellipse → inward spiral
timegraph(s, (1010, 175), 90); // x(t), v(t) — envelope shrinks
well(s, (760, 460), 90); // ball settles to the bottom of the parabola
energygraph(s, (1010, 460), 90); // total energy decays
text(cap, (cx, h - 32), "damping = 0.6 · the spiral, settling ball, and decaying total energy all show the same loss");
size(cap, 16); color(cap, dim); display(cap);
run(s, 10);
spring-annotated
Elevating the spring with a TYPEWRITER lab-note (type + cursor) and LIVE COUNTERS
(counter + to(_, value, …)) ticking k and the period up — Hooke’s law → parabolic
well → SHM, with no stage-covering section cards. One of three elevation styles.
// ============================================================================
// spring-annotated.manic — ELEVATE a sim: typewriter narration + live data
// ----------------------------------------------------------------------------
// One elevation flavour of several (see pulley-annotated for camera work,
// brachistochrone-annotated for kinetic typography). Here the story is told by
// a TYPEWRITER lab-note (`type` + `cursor`) and LIVE COUNTERS (`counter` +
// `to(_, value, …)`) that tick up — no stage-covering section cards, so the
// motion is never hidden. The spring's parts are ordinary entities the whole
// language drives directly.
// ============================================================================
title("Anatomy of a Spring — Hooke's Law & SHM");
canvas("16:9");
text(hdr, (cx, 40), "Anatomy of a Spring"); size(hdr, 28); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// ---- the sim + views (staged) ----
spring(sp, (340, 320), 10, 1.4, 105);
hidden(sp.wall); hidden(sp.spring); hidden(sp.mass); hidden(sp.overlays);
untraced(sp.path);
well(sp, (1015, 230), 112); hidden(sp.well);
energygraph(sp, (1015, 480), 112); hidden(sp.energy);
// ---- part annotations ----
line(eq, (340, 268), (340, 372)); color(eq, dim); stroke(eq, 2); untraced(eq);
text(coilL, (285, 232), "spring, stiffness k"); size(coilL, 16); color(coilL, lime); display(coilL); hidden(coilL);
text(massL, (520, 272), "mass m"); size(massL, 16); color(massL, cyan); display(massL); hidden(massL);
bracelabel(xb, (340, 372), (487, 372), "x₀", 22); color(xb, gold); hidden(xb); hidden(xb.label);
text(hooke, (340, 168), "F = −k·x"); size(hooke, 24); color(hooke, gold); display(hooke); hidden(hooke);
// ---- a TYPEWRITER lab-note with a live cursor (the narration device) ----
text(note, (cx, h - 44), ""); size(note, 21); color(note, fg); display(note); cursor(note);
text(kick, (96, 92), ""); size(kick, 17); color(kick, magenta); bold(kick); display(kick);
// ---- LIVE COUNTERS (tick up on reveal) ----
counter(kC, (150, 470), 0, 1, "k = ", " N/m"); size(kC, 26); color(kC, lime); display(kC); hidden(kC);
counter(tC, (150, 512), 0, 2, "T = ", " s"); size(tC, 26); color(tC, cyan); display(tC); hidden(tC);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(kick, "SETUP", 0.2);
say(note, "a wall, a coil of stiffness k, and a mass", 0.1); type(note, 1.4);
show(sp.wall, 0.4); pulse(sp.wall); show(coilL, 0.4);
show(sp.spring, 0.4); flash(sp.spring, lime); show(sp.mass, 0.4); pulse(sp.mass); show(massL, 0.4);
show(kC, 0.3); to(kC, value, 10, 0.8);
wait(0.5);
say(kick, "HOOKE'S LAW", 0.2);
say(note, "pull it x₀ from rest — it pulls back, F = −k·x", 0.1); type(note, 1.7);
draw(eq, 0.4); show(xb, 0.4); show(xb.label, 0.4); show(hooke, 0.4); flash(sp.spring, gold);
show(tC, 0.3); to(tC, value, 1.99, 0.9); // period 2π√(m/k)
wait(0.6);
fade(coilL, 0.3); fade(massL, 0.3); fade(xb, 0.3); fade(xb.label, 0.3); fade(hooke, 0.3);
wait(0.2);
say(kick, "MOTION", 0.2);
say(note, "release — simple harmonic motion; every panel tells the same swing", 0.1); type(note, 1.9);
show(sp.well, 0.5); show(sp.overlays, 0.4); show(sp.energy, 0.5); draw(sp.path, 0.6);
run(sp, 10);
spring-paper
The SAME spring sim dressed as a textbook figure AND run: template("paper") inks it,
a hatched support wall, a forest-green coil and outlined mass box, Hooke’s law and x₀
revealed, then run plays the SHM — the paper treatment on a LIVE sim (see pulley-paper).
// ============================================================================
// spring-paper.manic — an ANIMATED sim in textbook paper style
// ----------------------------------------------------------------------------
// The `spring` sim (mass on a spring, SHM), dressed as a textbook figure AND run:
// `template("paper")` inks it automatically, a hatched `support` wall, the coil
// in forest green, an outlined mass box, plus a base-manic reveal (Hooke's law,
// equilibrium, x₀) — then `run` plays the oscillation. The companion to
// pulley-paper: the paper/support treatment on another live sim.
// ============================================================================
title("Mass on a spring — textbook style");
canvas("16:9");
template("paper");
text(hdr, (cx, 46), "Mass on a spring — Hooke's law"); color(hdr, fg); size(hdr, 26); bold(hdr); display(hdr); hidden(hdr);
// the sim, restyled to textbook ink (parts hidden, revealed in order)
spring(sp, (380, 300), 10, 1.3, 110);
outlined(sp.mass); outline(sp.mass, fg);
hidden(sp.wall); hidden(sp.spring); hidden(sp.mass); hidden(sp.overlays);
untraced(sp.path);
// a hatched wall stands in for the sim's plain anchor; equilibrium reference
support(wall, (177, 300), 150, "right"); untraced(wall);
line(eq, (380, 246), (380, 354)); color(eq, dim); stroke(eq, 2); untraced(eq);
text(eqL, (380, 232), "equilibrium"); color(eqL, dim); size(eqL, 15); display(eqL); hidden(eqL);
text(coilL, (300, 214), "spring, stiffness k"); color(coilL, fg); size(coilL, 17); display(coilL); hidden(coilL);
text(massL, (548, 258), "mass m"); color(massL, fg); size(massL, 17); display(massL); hidden(massL);
bracelabel(xb, (380, 360), (523, 360), "x₀", 22); color(xb, fg); hidden(xb); hidden(xb.label);
text(hooke, (380, 152), "F = −k·x"); color(hooke, fg); size(hooke, 24); display(hooke); hidden(hooke);
text(cap, (cx, h - 40), ""); color(cap, fg); size(cap, 20); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(cap, "a coil of stiffness k fixed to a wall, with a mass on the end", 0.4);
draw(wall, 0.4); show(sp.spring, 0.4); show(coilL, 0.4); show(sp.mass, 0.4); show(massL, 0.3);
wait(0.4);
say(cap, "pull it x₀ from equilibrium — it pulls straight back, F = −k·x", 0.4);
draw(eq, 0.4); show(eqL, 0.3); show(xb, 0.4); show(xb.label, 0.4); show(hooke, 0.5);
wait(0.7);
fade(coilL, 0.3); fade(massL, 0.3); fade(xb, 0.3); fade(xb.label, 0.3); fade(hooke, 0.3);
say(cap, "release — simple harmonic motion", 0.4);
run(sp, 9);
double-pendulum
Deterministic chaos: two arms hinged end-to-end whose outer bob traces a wild,
unrepeatable curve — yet the render is frame-identical every run. A 4-D system, so
it shows phase (θ₁ vs θ₂) and energygraph but has no potential well
(doublependulum + views + run).
// ============================================================================
// double-pendulum.manic — chaos, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `doublependulum(id, [center], [angle1], [angle2], [unit])` — two arms hinged
// end-to-end: deterministic, yet exquisitely sensitive to initial conditions.
// PRE-SIMULATED with RK4 (so the render is frame-identical every run). It's a
// 4-D system, so it inherits `phase` (θ₁ vs θ₂), `timegraph`, and `energygraph`
// — but NOT `well` (there's no single-variable potential). `run(id,[dur])` plays
// it; drawing `{id}.path` in parallel traces the outer bob's chaotic curve.
//
// TO ADAPT: nudge angle2 by one degree and re-run — the trail diverges completely.
// ============================================================================
title("Double pendulum — deterministic chaos");
canvas("16:9");
text(hdr, (cx, 44), "Double pendulum — deterministic, yet unpredictable");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
// the chaotic sim on the left (outer bob traces the wild curve)
doublependulum(dp, (400, 250), 125, 110);
untraced(dp.path);
// the views that DO apply to a 4-D system (no potential well here)
phase(dp, (900, 190), 100); // θ₁ vs θ₂ — the coupled angles
energygraph(dp, (900, 480), 100); // KE / PE / total (total ~conserved)
text(cap, (cx, h - 32), "outer bob's trail is chaotic; total energy stays (nearly) conserved — no friction");
size(cap, 16); color(cap, dim); display(cap);
// trace the chaotic trail AS the pendulum swings (draw + run in parallel)
par {
run(dp, 12);
draw(dp.path, 12);
}
spring-pendulum
An elastic pendulum — a bob on a springy rod (drawn as a stretching coil) that both
swings and bounces, energy sloshing between the two modes (springpendulum).
// spring-pendulum.manic — the elastic pendulum: swings AND bounces.
// springpendulum(id,[center],[angle0],[stretch0],[unit],[damping]) — a bob on a
// springy rod (drawn as a stretching coil). Energy sloshes between the swing and
// the bounce, so the phase portrait and energy graph are richer than a rigid rod.
title("Elastic pendulum — swing meets bounce");
canvas("16:9");
text(hdr,(cx,44),"Spring pendulum — energy sloshes between swing and bounce");
size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
springpendulum(sp, (380,230), 35, 0.5, 105);
untraced(sp.path);
phase(sp, (900,190), 95); // θ vs ω
energygraph(sp, (900,470), 95); // KE / PE / total (~conserved, lightly damped)
text(cap,(cx,h-32),"the coil stretches as it swings — a two-mode system"); size(cap,16); color(cap,dim); display(cap);
par { run(sp, 11); draw(sp.path, 11); }
kapitza
The Kapitza pendulum: vibrate the pivot fast enough and the inverted position
becomes stable — the bob hovers near the top instead of falling (kapitza).
// kapitza.manic — vibrate the pivot fast enough and the pendulum stands UP.
// kapitza(id,[center],[angle0deg],[vibeamp],[unit]) — a driven (Kapitza) pendulum;
// with a strong enough vibration the INVERTED position becomes stable.
title("Kapitza pendulum — stable upside-down");
canvas("16:9");
text(hdr,(cx,44),"Kapitza pendulum — fast pivot vibration stabilises 'up'");
size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
kapitza(kp, (640,430), 165, 240, 150); // start near inverted, strong vibration
untraced(kp.path);
text(cap,(cx,h-32),"the gold pivot bobs fast; the bob hovers near the top (inverted)"); size(cap,16); color(cap,dim); display(cap);
par { run(kp, 9); draw(kp.path, 9); }
cart-pendulum
A pendulum on a spring-mounted cart rolling on a track — the classic control-theory
system; cart and bob trade momentum and energy (cartpendulum).
// cart-pendulum.manic — a pendulum on a spring-mounted cart (the control classic).
// cartpendulum(id,[center],[angle0deg],[unit]) — cart rolls on a track (spring to a
// wall) while the pendulum swings; the two exchange momentum and energy.
title("Cart-pendulum — coupled motion");
canvas("16:9");
text(hdr,(cx,44),"Cart-pendulum — the cart and bob trade momentum"); size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
cartpendulum(cp, (480,330), 55, 105);
phase(cp, (1000,190), 95); // θ vs ω
energygraph(cp, (1000,470), 95); // KE / PE / total (~conserved)
text(cap,(cx,h-32),"cart on a spring + swinging bob — energy stays (nearly) conserved"); size(cap,16); color(cap,dim); display(cap);
run(cp, 11);
compare-pendulum
Sensitive dependence: two identical driven pendulums started 0.001 rad apart drift
onto completely different paths — the butterfly effect, watched in phase/timegraph
(comparependulum).
// compare-pendulum.manic — sensitive dependence: two pendulums 0.001 rad apart.
// comparependulum(id,[center],[angle0deg],[unit]) — identical driven-damped
// physics, a hair-different start — yet they diverge completely (the butterfly effect).
title("Two pendulums, one hair apart");
canvas("16:9");
text(hdr,(cx,44),"Sensitive dependence — a 0.001 rad difference explodes"); size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
comparependulum(cm, (380,230), 12, 120);
phase(cm, (900,190), 95); // θ_A vs θ_B — starts on the diagonal, then scatters
timegraph(cm, (900,470), 95); // θ_A(t) & θ_B(t) diverge
text(cap,(cx,h-32),"cyan and magenta start together, then go their separate ways"); size(cap,16); color(cap,dim); display(cap);
run(cm, 13);
vertical-spring
A mass bobbing on a vertical spring under gravity — gravity shifts the equilibrium
but the energy well stays a parabola (verticalspring).
// vertical-spring.manic — a mass bobbing on a vertical spring under gravity.
title("Vertical spring — bobbing under gravity");
canvas("16:9");
text(hdr,(cx,44),"Vertical spring — a mass bobs about its stretched equilibrium"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
verticalspring(vs, (400,170), 0.7);
untraced(vs.path);
phase(vs, (900,190), 95); // d vs ḋ
well(vs, (900,470), 95); // parabolic well (shifted by gravity)
text(cap,(cx,h-32),"gravity shifts the equilibrium down; the well is still a parabola"); size(cap,16); color(cap,dim); display(cap);
par { run(vs, 10); draw(vs.path, 10); }
spring-incline
A mass on a spring on an inclined plane; gravity’s along-ramp component sets a new
stretched rest point it oscillates about (springincline).
// spring-incline.manic — a mass on a spring on an inclined plane.
title("Spring on an incline");
canvas("16:9");
text(hdr,(cx,44),"Spring on an incline — gravity's along-ramp pull shifts equilibrium"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
springincline(si, (360,180), 32);
untraced(si.path);
phase(si, (960,200), 100);
energygraph(si, (960,470), 100);
text(cap,(cx,h-32),"the bob oscillates along the ramp about its stretched rest point"); size(cap,16); color(cap,dim); display(cap);
par { run(si, 10); draw(si.path, 10); }
bungee
A bungee jump: free-fall, then a ONE-SIDED elastic cord (it only pulls) catches and
bounces the jumper — note the lopsided energy well (bungee).
// bungee.manic — free-fall, then a one-sided elastic cord catches the jumper.
title("Bungee jump — free-fall, then bounce");
canvas("16:9");
text(hdr,(cx,44),"Bungee — the cord only PULLS: free-fall, then an elastic bounce"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
bungee(bg, (640,110));
phase(bg, (1000,200), 95); // asymmetric well shows in the phase loop
well(bg, (1000,470), 95); // linear (fall) → parabola (cord): a lopsided well
text(cap,(cx,h-32),"note the lopsided energy well — flat during free-fall, steep once the cord bites"); size(cap,15); color(cap,dim); display(cap);
run(bg, 11);
resonance
A driven spring pushed near its natural frequency √(k/m): the amplitude climbs and
climbs — resonance, watched building up in phase/energygraph (resonance).
// resonance.manic — a driven spring pumped near its natural frequency.
title("Resonance — driving near the natural frequency");
canvas("16:9");
text(hdr,(cx,44),"Resonance — drive near √(k/m) and the amplitude grows and grows"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
resonance(rs, (400,300), 3.8); // natural freq = √(16/1) = 4; drive 3.8 ≈ resonance
untraced(rs.path);
phase(rs, (960,200), 100); // spiral OUT to a big steady-state loop
energygraph(rs, (960,470), 100); // energy climbs, then plateaus
text(cap,(cx,h-32),"drive frequency 3.8 vs natural 4.0 — near resonance, so it builds up big"); size(cap,15); color(cap,dim); display(cap);
par { run(rs, 12); draw(rs.path, 12); }
double-spring
Two masses coupled by springs between walls — push one and the energy sloshes back
and forth (beating); normal modes show as diagonals in phase (doublespring).
// double-spring.manic — two masses coupled by springs: energy sloshes (beating).
title("Coupled springs — energy sloshes back and forth");
canvas("16:9");
text(hdr,(cx,44),"Two coupled masses — push one, and the energy beats between them"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
doublespring(dd, (430,300), 85);
phase(dd, (980,200), 100); // x1 vs x2 — normal modes are diagonals
energygraph(dd, (980,470), 100);
text(cap,(cx,h-32),"block 1 (cyan) starts displaced; watch block 2 (magenta) pick up its swing"); size(cap,15); color(cap,dim); display(cap);
run(dd, 12);
series-parallel-springs
The same mass on springs in series (soft, slow) vs parallel (stiff, fast), side by
side — the timegraph makes the frequency difference obvious (seriesparallel).
// series-parallel-springs.manic — same mass, springs in series vs parallel.
title("Series vs parallel springs");
canvas("16:9");
text(hdr,(cx,44),"Same mass, same springs — series is soft (slow), parallel is stiff (fast)"); size(hdr,21); color(hdr,cyan); bold(hdr); display(hdr);
seriesparallel(sp, (560,150), 68);
timegraph(sp, (1060,320), 95); // y_s(t) vs y_p(t): parallel oscillates faster
text(cap,(cx,h-30),"parallel adds stiffness (k₁+k₂); series divides it (1/k = 1/k₁+1/k₂)"); size(cap,15); color(cap,dim); display(cap);
run(sp, 11);
car-suspension
A quarter-car riding a scrolling road — a speed bump, a washboard stretch, and a
pothole — its spring+damper soaking up the ride (carsuspension).
// car-suspension.manic — a quarter-car riding a road: bump, washboard, pothole.
title("Car suspension — riding the road");
canvas("16:9");
text(hdr,(cx,44),"Quarter-car suspension — the body bobs as the wheel rides the road"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
carsuspension(car, (640,430));
energygraph(car, (1050,170), 100);
text(cap,(cx,h-30),"a speed bump, a washboard stretch, then a pothole — the spring+damper soak it up"); size(cap,15); color(cap,dim); display(cap);
run(car, 12);
car-suspension-annotated
A marketing hero: the quarter-car suspension on a template("paper") brochure page,
elevated with generic base-manic — a live counter (sprung mass), leader-arrow
callouts, and an energygraph of the shock being absorbed — riding a scrolling road.
// ============================================================================
// car-suspension-annotated.manic — a marketing hero, paper (brochure) style
// ----------------------------------------------------------------------------
// The `carsuspension` quarter-car sim, dressed as a clean product diagram on a
// `template("paper")` page and ELEVATED with generic base-manic: a live
// `counter` (the sprung mass ticking up), leader-`arrow` callouts, a
// `bracelabel` for the travel, and a compact `energygraph` showing the shock
// being absorbed — then it rides a scrolling road (bump · washboard · pothole).
// Every part of the sim is an ordinary entity, so it all composes.
// ============================================================================
title("Car suspension — smooth over every bump");
canvas("16:9");
template("paper");
// ---- hero title ----
text(hdr, (cx, 52), "Car suspension: how it soaks up the road"); color(hdr, fg); size(hdr, 30); bold(hdr); display(hdr); hidden(hdr);
text(sub, (cx, 88), "the quarter-car model — sprung mass · spring + damper · wheel"); color(sub, dim); size(sub, 18); display(sub); hidden(sub);
// ---- the sim, recoloured for a bold, clean brochure look ----
carsuspension(car, (430, 430));
color(car.body, blue); // the car body — bold accent
color(car.road, fg); // the road — dark ink
hidden(car); // reveal the whole sim (road, wheel, spring, body, energy) together
// a compact energy panel (top-right) — the suspension absorbing the shock
energygraph(car, (1055, 215), 88);
// ---- generic-kit callouts (leader arrows + labels), hidden to reveal in order ----
text(cb, (640, 296), "car body — the sprung mass"); color(cb, blue); size(cb, 18); display(cb); hidden(cb);
arrow(ab, (636, 302), (478, 320)); color(ab, dim); stroke(ab, 2); hidden(ab);
text(csp, (610, 382), "spring + damper"); color(csp, lime); size(csp, 18); display(csp); hidden(csp);
arrow(asp, (606, 384), (448, 382)); color(asp, dim); stroke(asp, 2); hidden(asp);
text(cw, (620, 472), "wheel — the unsprung mass"); color(cw, gold); size(cw, 18); display(cw); hidden(cw);
arrow(aw, (616, 470), (448, 434)); color(aw, dim); stroke(aw, 2); hidden(aw);
text(cr, (cx, 648), "the road: a bump, a washboard stretch, then a pothole"); color(cr, fg); size(cr, 17); display(cr); hidden(cr);
// a live spec counter (generic kit) — the sprung mass ticking up
counter(mc, (150, 320), 0, 0, "m = ", " kg"); color(mc, fg); size(mc, 30); display(mc); hidden(mc);
text(spec, (cx, h - 28), "quarter-car · k = 20 kN/m · c = 4 kN·s/m · v = 8 m/s"); color(spec, dim); size(spec, 16); display(spec); hidden(spec);
// ============================== SCRIPT ==============================
show(hdr, 0.5); show(sub, 0.4);
wait(0.3);
show(car, 0.6);
wait(0.3);
show(cb, 0.3); draw(ab, 0.3);
show(csp, 0.3); draw(asp, 0.3);
show(cw, 0.3); draw(aw, 0.3);
show(cr, 0.3);
show(mc, 0.3); to(mc, value, 500, 0.9); // the sprung mass counts up
show(spec, 0.4);
wait(0.5);
// the ride — the road scrolls, the body glides while the wheel tracks every bump
run(car, 11);
piston
An engine piston: a spinning crank + connecting rod turn rotation into the piston’s
up-and-down stroke — the slider-crank mechanism (piston).
// piston.manic — an engine piston: a spinning crank drives a slider in a cylinder.
title("Engine piston — the slider-crank");
canvas("16:9");
text(hdr,(cx,46),"Slider-crank — a spinning crank becomes up-and-down motion"); size(hdr,23); color(hdr,cyan); bold(hdr); display(hdr);
piston(eng, (cx, 480), 60);
text(cap,(cx,h-32),"gold crank sweeps the circle; the rod converts it to the piston's stroke"); size(cap,16); color(cap,dim); display(cap);
run(eng, 9);
molecule
A molecule as balls and springs — atoms bonded on every side, vibrating about their
equilibrium shape with the total energy conserved (molecule).
// molecule.manic — atoms bonded by springs, vibrating about their shape.
title("Vibrating molecule");
canvas("16:9");
text(hdr,(cx,46),"A molecule as balls and springs — bonds stretch and the atoms jiggle"); size(hdr,22); color(hdr,cyan); bold(hdr); display(hdr);
molecule(mol, (520, 300), 3);
energygraph(mol, (1000, 300), 110); // total energy is conserved as it vibrates
text(cap,(cx,h-32),"one atom starts pulled out; the bonds pull it back and the whole thing rings"); size(cap,15); color(cap,dim); display(cap);
run(mol, 11);
robot-arm
A two-link robot arm reaching for a target: the joint rates come from the analytic
inverse Jacobian, so the arm drives its end-effector to the goal and settles there —
inverse kinematics as a solved motion (robotarm).
// ============================================================================
// robot-arm.manic — inverse kinematics, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `robotarm(id, [center], [mode], [unit])` — a two-link arm that tracks a target
// by inverse kinematics. Its joint rates are the analytic 2×2 inverse Jacobian
// times a gain on the end-effector error, so the gripper chases the target.
// `mode` 1 = trace a circle (default), 2 = figure-8, 0 = reach a fixed point and
// settle. PRE-SIMULATED with RK4; `run(id,[dur])` replays it, and `{id}.path`
// traces the route the gripper sweeps.
//
// TO ADAPT: set mode 2 for a figure-8, or mode 0 to reach one fixed point.
// ============================================================================
title("Robot arm — tracking by inverse kinematics");
canvas("16:9");
text(hdr, (cx, 46), "Two-link arm — inverse kinematics keeps the gripper on the moving target");
size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
robotarm(rb, (cx, 470), 1);
text(cap, (cx, h - 32), "gold shoulder + cyan forearm chase the lime ring around its circle — the gripper traces the path");
size(cap, 15); color(cap, dim); display(cap);
run(rb, 10);
pulley
The Atwood machine: two masses over one pulley, the heavier one accelerating down at
(m₁−m₂)g/(m₁+m₂). energygraph shows kinetic energy climbing as potential falls
(pulley).
// ============================================================================
// pulley.manic — the Atwood machine, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `pulley(id, [center], [m1], [m2], [unit])` — two masses over one pulley. The
// heavier one accelerates down at a = (m₁−m₂)g/(m₁+m₂). PRE-SIMULATED with RK4;
// `run(id,[dur])` replays it. `energygraph` shows the KE↔PE trade as the system
// speeds up (total is conserved — no friction).
//
// TO ADAPT: set m1 and m2 closer together for a gentler acceleration.
// ============================================================================
title("Atwood machine — unequal masses accelerate");
canvas("16:9");
text(hdr, (cx, 44), "Atwood machine — a = (m₁−m₂)g / (m₁+m₂)");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
pulley(pl, (440, 170), 3, 2);
energygraph(pl, (960, 330), 120);
text(cap, (cx, h - 32), "cyan m₁ (heavier) sinks, magenta m₂ rises; kinetic energy climbs as potential falls");
size(cap, 16); color(cap, dim); display(cap);
run(pl, 5);
pulley-scale
The surprise every physics class remembers: an in-line spring scale on an Atwood
machine reads the rope TENSION 2·m₁·m₂·g/(m₁+m₂) — not the sum of the two weights
(pulleyscale).
// ============================================================================
// pulley-scale.manic — what does the scale read? (physics Layer 1)
// ----------------------------------------------------------------------------
// `pulleyscale(id, [center], [m1], [m2], [unit])` — an Atwood machine over two
// pulleys with a spring scale in the rope between them. The classic surprise:
// the scale reads the rope TENSION 2·m₁·m₂·g/(m₁+m₂), NOT the sum of the two
// weights. PRE-SIMULATED with RK4; `run(id,[dur])` replays it.
//
// TO ADAPT: set m1 = m2 — the system balances and the scale reads exactly m·g.
// ============================================================================
title("Pulley scale — it reads the tension, not the weight");
canvas("16:9");
text(hdr, (cx, 46), "The scale reads the rope tension T = 2·m₁·m₂·g / (m₁+m₂)");
size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
pulleyscale(ps, (cx, 200), 4, 3);
text(cap, (cx, h - 32), "not (m₁+m₂)g, not the heavier weight — the tension sits between the two weights");
size(cap, 16); color(cap, dim); display(cap);
run(ps, 5);
block-tackle
A compound pulley (block & tackle): a load on a movable block held by N rope strands,
pulled by an effort mass. N strands = a mechanical advantage of N — an effort of only
load/N balances the load, but the effort end travels N× as far (blocktackle).
// ============================================================================
// block-tackle.manic — the compound pulley (mechanical advantage), Layer 1
// ----------------------------------------------------------------------------
// `blocktackle(id, [center], [load], [effort], [strands], [unit])` — a load on a
// movable block held by N rope strands and pulled by an effort mass. The N
// strands give a MECHANICAL ADVANTAGE of N: an effort of only load/N balances
// the load, and the effort end travels N× as far as the load rises. PRE-SIMULATED
// with RK4; `run(id,[dur])` replays it. N = 1 is just the Atwood machine.
//
// TO ADAPT: change `strands` (1–4) — more strands lift the same load with less
// effort, but the effort mass has to travel that much further.
// ============================================================================
title("Block & Tackle — a compound pulley's mechanical advantage");
canvas("16:9");
text(hdr, (cx, 40), "Block & tackle — 3 strands lift 8 kg with 3 kg of effort");
size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
blocktackle(bt, (500, 130), 8, 3, 3);
energygraph(bt, (1030, 330), 120);
text(cap, (cx, h - 32), "3 strands ⇒ MA = 3: effort just over load/3 lifts it; the effort end travels 3× as far");
size(cap, 15); color(cap, dim); display(cap);
run(bt, 5);
compound-pulley
A compound pulley with a MOVABLE pulley: a fixed top pulley carries mass A on one side
and a movable lower pulley on the other; that pulley carries B and C. The string
constraints link them (a_A = −a_P, a_B + a_C = 2·a_P); static when mA = mB+mC
(compoundpulley).
// ============================================================================
// compound-pulley.manic — fixed + movable pulley, three masses (physics L1)
// ----------------------------------------------------------------------------
// `compoundpulley(id, [center], [mA], [mB], [mC], [unit])` — a fixed top pulley
// carries mass A on one side and a MOVABLE lower pulley on the other; the movable
// pulley carries masses B and C. The string constraints link them: a_A = −a_P and
// a_B + a_C = 2·a_P (the massless movable pulley gives T₁ = 2·T₂). Static exactly
// when mA = mB + mC. PRE-SIMULATED with RK4; `run(id,[dur])` replays it.
//
// TO ADAPT: set mA = mB + mC and it balances; make A heavier and it hauls B, C up.
// ============================================================================
title("Compound pulley — fixed + movable, masses A, B, C");
canvas("16:9");
text(hdr, (cx, 40), "Compound pulley — heavier A hauls B and C upward");
size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);
compoundpulley(cp, (470, 120), 5, 2, 2);
energygraph(cp, (1010, 330), 120);
text(cap, (cx, h - 32), "A (5 kg) > B+C (4 kg): A sinks, the movable pulley rises, B and C are pulled up together");
size(cap, 15); color(cap, dim); display(cap);
run(cp, 4);
incline-pulley
The incline-Atwood: a block on an incline tied over a pulley at the top to a hanging
mass. m₂ outpulls m₁·sinθ, so the block climbs while the mass descends — energygraph
tracks the KE↔PE trade (inclinepulley).
// incline-pulley.manic — the incline-Atwood: a block on an incline tied over a
// pulley at the top to a hanging mass. `inclinepulley(id,[center],[angle],[m1],
// [m2],[unit])`. PRE-SIMULATED (RK4); `run(id)` plays it; `energygraph` shows the
// KE↔PE trade. Matches the classic textbook figure.
title("Incline + pulley — the incline-Atwood");
canvas("16:9");
text(hdr, (cx, 44), "Block on an incline, tied over a pulley to a hanging mass"); size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
inclinepulley(ip, (280, 500), 30, 3, 2);
energygraph(ip, (1010, 320), 120);
text(cap, (cx, h - 32), "m₂ (2 kg) hanging outpulls m₁·sinθ, so m₁ climbs the incline and m₂ descends"); size(cap, 15); color(cap, dim); display(cap);
run(ip, 4);
double-incline
Two blocks on a wedge’s two slopes, tied over a pulley at the apex (right slope rough).
The 70 kg block on the gentle 30° slope beats the 12 kg block on the steep 50° smooth
slope — connected motion on two inclines (doubleincline).
// double-incline.manic — two blocks on a wedge's two slopes, tied over a pulley
// at the apex. `doubleincline(id,[center],[angle1],[angle2],[m1],[m2],[unit])`;
// the right slope is rough. PRE-SIMULATED (RK4); `run(id)` plays it.
title("Double incline — connected blocks on two slopes");
canvas("16:9");
text(hdr, (cx, 42), "Two slopes, one rope over the apex — which way does it slide?"); size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
doubleincline(di, (cx, 520), 50, 30, 12, 70);
text(cap, (cx, h - 30), "M₂ (70 kg) on the gentle rough slope beats M₁ (12 kg) on the steep smooth one"); size(cap, 15); color(cap, dim); display(cap);
run(di, 5);
incline-bumper
A block slides down an incline into a spring bumper at the base, compresses it, and
launches back up — one-sided contact, gravity PE ↔ kinetic ↔ spring PE, energy
conserved (inclinebumper).
// incline-bumper.manic — a block slides down an incline into a spring bumper at
// the base, compresses it, and launches back up (one-sided contact).
// `inclinebumper(id,[center],[angle],[mass],[stiffness],[unit])`. PRE-SIMULATED
// (RK4); `energygraph` shows gravity PE ↔ kinetic ↔ spring PE (conserved).
title("Incline + spring bumper — slide, compress, launch");
canvas("16:9");
text(hdr, (cx, 44), "A block slides down and compresses a spring at the base, then launches back"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
inclinebumper(ib, (300, 500), 40, 2, 500);
energygraph(ib, (1010, 320), 120);
text(cap, (cx, h - 32), "gravity PE → kinetic → spring PE → back: one-sided contact, energy conserved"); size(cap, 15); color(cap, dim); display(cap);
run(ib, 6);
collide-blocks
The classic momentum demo: block 1 hangs on a spring to the wall, block 2 slides in and
they collide. A live Σp readout shows momentum conserved at every collision; elastic
(e=1) keeps total energy flat while it sloshs between KE and the spring (collideblocks).
// collide-blocks.manic — the classic momentum demo. Block 1 (left) is attached to
// the wall by a SPRING; block 2 slides in freely and they collide with restitution
// e (1 = elastic → energy conserved; <1 → lost). The live Σp readout (top) shows
// momentum is conserved at every collision. Built on the shared `collide_1d`.
title("Colliding blocks — momentum is conserved");
canvas("16:9");
text(hdr, (cx, 96), "Block 1 on a spring, block 2 sliding in — watch the momentum readout"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
collideblocks(cb, (cx, 440), 3, 1, 1);
energygraph(cb, (1040, 210), 90);
text(cap, (cx, h - 36), "elastic (e=1): total energy (KE + spring PE) is conserved; Σp is conserved at each collision"); size(cap, 15); color(cap, dim); display(cap);
run(cb, 10);
collide-blocks-annotated
Conservation of momentum, the MANIC way — not a 1:1 port of the lab sim but a guided lesson: the live Σp readout as the star, the KE↔spring-PE energy view, staged callouts, and honest narration (with a wall-spring, Σp is conserved AT each collision, not constant).
// ============================================================================
// collide-blocks-annotated.manic — conservation of momentum, the MANIC way
// ----------------------------------------------------------------------------
// Not a 1:1 port of the lab sim — a guided lesson that ELEVATES it: the live Σp
// readout, the KE↔spring-PE energy view, staged callouts, and honest narration
// (with a wall-spring, Σp is conserved AT each collision — Newton's 3rd law — not
// constant, since the spring is an external force). All base manic over the sim.
// ============================================================================
title("Conservation of Momentum");
canvas("16:9");
// the sim's Σp readout sits at the very top (y≈46) — the star of the scene
collideblocks(cb, (cx, 460), 3, 1, 1);
hidden(cb); // the bare id hides every part + the readout + energy view
energygraph(cb, (1055, 250), 84); // KE ↔ spring PE (tagged cb, revealed by show(cb))
text(hdr, (cx, 104), "Conservation of Momentum"); size(hdr, 30); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
text(sub, (cx, 140), "block 1 on a spring · block 2 slides in · watch Σp through the collision"); size(sub, 17); color(sub, dim); display(sub); hidden(sub);
// callouts
text(csp, (360, 330), "spring holds block 1 to the wall"); size(csp, 16); color(csp, lime); display(csp); hidden(csp);
arrow(asp, (392, 344), (418, 424)); color(asp, dim); stroke(asp, 2); hidden(asp);
text(cb1, (470, 300), "block 1 · m₁ = 3 kg"); size(cb1, 16); color(cb1, cyan); display(cb1); hidden(cb1);
text(cb2, (800, 300), "block 2 · m₂ = 1 kg →"); size(cb2, 16); color(cb2, magenta); display(cb2); hidden(cb2);
text(cap, (cx, h - 38), ""); size(cap, 19); color(cap, fg); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5); show(sub, 0.4);
wait(0.3);
show(cb, 0.6); // sim + Σp readout + energy view
show(csp, 0.3); draw(asp, 0.3); show(cb1, 0.3); show(cb2, 0.3);
wait(0.4);
say(cap, "block 2 slides toward block 1, which is held by its spring", 0.4);
wait(0.5);
say(cap, "they collide — each pushes the other equal and opposite (Newton's 3rd law)", 0.4);
flash(cb.mom, gold);
wait(0.5);
say(cap, "so Σp doesn't jump at the hit — the spring only shifts it BETWEEN collisions", 0.4);
wait(0.5);
say(cap, "energy sloshes between motion and the spring, but the total stays put — elastic", 0.4);
run(cb, 12);
bullet-block
A bullet fired into a block EMBEDS (perfectly inelastic). The flight is slow-mo so you can
watch it cross, then a live speed readout collapses from 40 m/s to ~1 — momentum survives,
energy does not. Uses collide_1d(e=0) (bulletblock).
// bullet-block.manic — a bullet fired into a block EMBEDS (perfectly inelastic).
// `bulletblock(id,[center],[bulletmass],[speed],[blockmass],[unit])`. The combined
// mass crawls off at m_b·v_b/(m_b+M) — most of the kinetic energy is lost to the
// collision, so `energygraph`'s total STEPS DOWN at impact. Uses `collide_1d(e=0)`.
title("Bullet into a block — an inelastic collision");
canvas("16:9");
text(hdr, (cx, 70), "The bullet embeds: 40 m/s becomes ~1 m/s — most of the energy is gone"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
bulletblock(bb, (cx, 430), 0.05, 40, 1.95);
energygraph(bb, (1040, 200), 90);
text(cap, (cx, h - 36), "momentum is conserved, but kinetic energy is NOT — it drops sharply at impact"); size(cap, 15); color(cap, dim); display(cap);
run(bb, 6);
bullet-impact
BEST OF BOTH: the cinematic gun-shot (gun · muzzle flash · a flying cam/zoom · BOOM)
wrapped around the REAL bulletblock physics — the collision is genuinely inelastic, the
live speed readout actually collapses 40 → ~1, and the BOOM is synced to the true impact.
// ============================================================================
// bullet-impact.manic — best of both: the movie AND the physics
// ----------------------------------------------------------------------------
// The cinematic gun-shot (gun · muzzle flash · a flying camera · BOOM) wrapped
// around the REAL `bulletblock` physics sim. The collision is genuinely true —
// a perfectly inelastic impact, momentum conserved, the live speed readout
// actually collapsing 40 → ~1 — and the BOOM is SYNCED to the physical impact.
// A sim's parts are ordinary entities, so the story and the physics compose.
// ============================================================================
title("Impact — the movie meets the physics");
canvas("16:9");
// ---- the REAL physics: bullet + block, inelastic collision + live speed readout ----
// a lighter block, so the hit visibly KNOCKS it and it slides on afterward
bulletblock(bb, (cx, 430), 0.05, 40, 0.9);
hidden(bb);
sticky(bb.vel); // pin the live speed readout through the camera moves
// ---- cinematic dressing (base manic) ----
rect(barrel, (200, 380), 82, 20); color(barrel, dim); filled(barrel);
rect(body, (166, 386), 42, 34); color(body, dim); filled(body);
polygon(grip, (150, 404), (176, 404), (170, 442), (146, 438), dim);
circle(mflash, (242, 380), 28); color(mflash, gold); glow(mflash, 3.5); hidden(mflash);
text(boom, (730, 250), "BOOM!"); size(boom, 84); color(boom, magenta); bold(boom); glow(boom, 2.5); display(boom); hidden(boom);
for i in 0..14 {
let ang = i * tau / 14.0;
line(spark{i}, (730, 380), (730 + 150*cos(ang), 380 + 150*sin(ang)));
color(spark{i}, gold); stroke(spark{i}, 5); glow(spark{i}, 2); untraced(spark{i}); tag(spark{i}, sparks);
}
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 22); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
cam((430, 400), 0.4, smooth);
say(cap, "a scene — and it's really physics underneath", 0.4);
wait(0.5);
say(cap, "FIRE!", 0.2);
par { show(mflash, 0.06); pulse(mflash); show(bb, 0.1); }
fade(mflash, 0.3);
// the bullet flies (REAL physics, slow-mo) · the camera pushes toward the block ·
// the BOOM fires exactly when the sim's bullet embeds (55% of the run) · then it
// KEEPS GOING — the block is knocked, slides on, and the camera follows it
par {
run(bb, 7);
cam((720, 380), 3.6, smooth); // push toward the impact during the flight
zoom(1.2, 3.6, smooth);
seq {
wait(3.85); // the physical impact (55% of the run)
// CONTACT → zoom-punch + BOOM
say(cap, "CONTACT!", 0.12);
par { flash(bb.block, gold); shake(bb.block, 0.6); zoom(1.6, 0.15); show(boom, 0.12); pulse(boom); draw(sparks, 0.3); }
// …and it CONTINUES — sparks clear, the block slides on, camera follows
say(cap, "…knocked back — it slides on", 0.3);
par { fade(sparks, 0.5); fade(boom, 0.7); cam((1010, 380), 2.4, smooth); zoom(1.15, 2.4, smooth); }
}
}
// it comes to rest — and the physics was true the whole way through
par { zoom(1.0, 0.9, smooth); cam((cx, 360), 0.9, smooth); }
say(cap, "…and rest. A movie on top, real physics underneath: 40 m/s → the block's crawl", 0.4);
wait(1.0);
bullet-block-annotated
The bullet’s JOURNEY, the manic way: a gun fires, a muzzle flash, a glowing bullet crosses
the gap in slow-motion and embeds — the speed readout crashing 40 → ~1. A scene, not the
bare lab sim (base-manic staging over bulletblock).
// ============================================================================
// bullet-block-annotated.manic — a bullet's journey (inelastic collision)
// ----------------------------------------------------------------------------
// Not the bare lab sim — a scene: a gun fires, a muzzle flash, a glowing bullet
// crosses the gap in slow-motion, embeds in the block, and the live speed readout
// COLLAPSES from 40 m/s to ~1. The block barely lurches — it soaked up the bullet
// but almost none of the speed. Base-manic staging over the `bulletblock` sim.
// ============================================================================
title("A bullet fired into a block");
canvas("16:9");
text(hdr, (cx, 44), "A bullet fired into a block"); size(hdr, 30); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
text(sub, (cx, 80), "perfectly inelastic: the bullet embeds — momentum survives, speed collapses"); size(sub, 17); color(sub, dim); display(sub); hidden(sub);
// the sim (bullet at the muzzle, block at rest, floor, live speed readout, energy)
bulletblock(bb, (cx, 430), 0.05, 40, 1.95);
hidden(bb);
energygraph(bb, (1055, 250), 82);
// the gun (all base-manic shapes), at the bullet's start height
rect(barrel, (200, 380), 82, 20); color(barrel, dim); filled(barrel);
rect(body, (166, 386), 42, 34); color(body, dim); filled(body);
polygon(grip, (150, 404), (176, 404), (170, 442), (146, 438), dim);
circle(flash, (242, 380), 17); color(flash, gold); glow(flash, 2.4);
hidden(barrel); hidden(body); hidden(grip); hidden(flash);
// callouts
text(cbl, (300, 336), "the bullet — light + fast"); size(cbl, 16); color(cbl, red); display(cbl); hidden(cbl);
text(cblk, (720, 300), "the block — heavy, at rest"); size(cblk, 16); color(cblk, cyan); display(cblk); hidden(cblk);
text(cap, (cx, h - 38), ""); size(cap, 19); color(cap, fg); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5); show(sub, 0.4);
wait(0.3);
show(bb, 0.5); show(barrel, 0.3); show(body, 0.3); show(grip, 0.3);
show(cbl, 0.3); show(cblk, 0.3);
wait(0.4);
say(cap, "a heavy block sits at rest; a fast, light bullet is loaded", 0.4);
wait(0.5);
say(cap, "FIRE — 40 m/s across the gap …", 0.4);
flash(flash, gold); pulse(flash);
run(bb, 6);
say(cap, "… it embeds. Momentum is conserved, but 40 m/s collapses to ~1 — the energy is gone", 0.4);
wait(1.0);
newtons-cradle
Newton’s cradle: pull one ball, one swings out the far side — momentum and energy pass
straight through the chain. An EVENT-DRIVEN sim (free-flight pendulums between elastic
collisions resolved by a shared 1-D impulse), the crowd-pleaser (newtonscradle).
// newtons-cradle.manic — Newton's cradle: pull N balls, N swing out. An EVENT-
// DRIVEN sim — free-flight pendulums between elastic collisions resolved by the
// shared `collide_1d` impulse. `newtonscradle(id,[center],[balls],[pulled])`.
title("Newton's cradle — momentum passes through");
canvas("16:9");
text(hdr, (cx, 70), "Pull one, one swings out — momentum and energy pass straight through"); size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
newtonscradle(nc, (cx, 170), 5, 1);
energygraph(nc, (1040, 470), 90);
text(cap, (cx, h - 40), "five equal balls, elastic collisions (e = 1): the chain conserves momentum and energy"); size(cap, 15); color(cap, dim); display(cap);
run(nc, 8);
string-wave
A wave on a plucked string: 36 masses on springs, both ends fixed (the discretised wave
equation). Pluck it off-centre and the pulse splits, travels, and reflects off the ends —
a rainbow chain that wiggles, pre-simulated with RK4 (stringwave).
// string-wave.manic — a wave on a plucked string: N masses on springs, both ends
// fixed (the discretised wave equation). `stringwave(id,[center],[width],[amp],
// [pluck])`. Pluck it off-centre and the pulse splits into two, travels out, and
// reflects (inverting) off the fixed ends. Drawn as a rainbow chain that wiggles.
title("Wave on a string — pluck it and watch it travel");
canvas("16:9");
text(hdr, (cx, 70), "Pluck a string: the pulse splits, travels, and reflects off the ends"); size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);
stringwave(sw, (cx, 380), 900, 110, 0.28);
text(cap, (cx, h - 40), "36 masses on springs, fixed at both ends — the wave equation, pre-simulated with RK4"); size(cap, 16); color(cap, dim); display(cap);
run(sw, 10);
loop-track
A ball rolls down a ramp and around a vertical LOOP-THE-LOOP — the curved-track case.
A bead energy solver (v = √(2g(H−y)) along the arc) so it visibly slows at the top;
release above 2·radius to clear it. energygraph tracks KE↔PE (looptrack).
// loop-track.manic — a ball rolls down a ramp and around a vertical loop-the-loop.
// `looptrack(id,[center],[radius],[height],[unit])`. A curved-track energy solver:
// v = √(2g(H−y)) along the arc, so the ball visibly SLOWS at the top. The release
// height must exceed 2·radius to clear the top. `energygraph` shows KE↔PE.
title("Loop-the-loop — down the ramp, around the loop");
canvas("16:9");
text(hdr, (cx, 44), "Release high enough (H > 2r) and the ball clears the top of the loop"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
looptrack(lt, (470, 560), 1, 3);
energygraph(lt, (1010, 330), 120);
text(cap, (cx, h - 32), "energy conserved: it trades kinetic for potential, slowest at the top of the loop"); size(cap, 15); color(cap, dim); display(cap);
run(lt, 5);
loop-cinematic
The loop-the-loop as a MOVIE with real physics inside: the camera pushes in as the ball
climbs, and the tension is genuine — a modest release height means it truly crawls over
the top before rocketing out. cam/zoom beats synced to the looptrack sim.
// ============================================================================
// loop-cinematic.manic — the loop-the-loop as a movie (real physics inside)
// ----------------------------------------------------------------------------
// Same recipe as bullet-impact: build the real `looptrack` sim, aim the camera,
// sync the beats to the physics. A ball is released, races down the ramp, and
// the camera pushes in as it climbs the loop — the tension is REAL (it genuinely
// slows at the top, energy traded for height), then it clears and rockets out.
// ============================================================================
title("Loop the Loop");
canvas("16:9");
// the real curved-track physics — a modest release height, so it BARELY clears
// the top (H just over 2·r) — that's where the real tension lives
looptrack(lt, (500, 560), 1, 2.4, 95);
hidden(lt);
text(cap, (cx, h - 46), ""); color(cap, fg); size(cap, 24); bold(cap); display(cap); sticky(cap);
// ================= THE SCENE =================
cam((420, 400), 0.4, smooth); // frame the whole ramp + loop
say(cap, "release…", 0.4);
show(lt, 0.5);
wait(0.4);
// down the ramp, into the loop — the camera pushes in for the climb
par {
run(lt, 6.0);
seq {
say(cap, "released — down the ramp!", 0.3);
par { cam((500, 455), 3.0, smooth); zoom(1.35, 3.0, smooth); } // slow push through the descent + entry
say(cap, "up and over — will it clear the top?!", 0.3);
wait(1.7); // the climb + the crawl over the top
say(cap, "…MADE IT!", 0.25);
par { cam((470, 410), 1.3, smooth); zoom(1.1, 1.3, smooth); } // ease back as it rockets out
}
}
// the physics was the whole story
say(cap, "…real energy: fastest at the bottom, barely crawling over the top", 0.4);
wait(1.0);
spring-chain
Three blocks joined by two springs on an incline — coupled oscillators. Pull one and the
whole chain rings (normal modes / beating); shown in the incline’s frame since uniform
gravity doesn’t touch the internal motion (springchain).
// spring-chain.manic — three blocks joined by two springs on an incline: coupled
// oscillators / normal modes. `springchain(id,[center],[angle],[unit])`. A uniform
// gravity component doesn't change the internal motion, so it's shown in the
// incline's frame (CM held). `energygraph` shows the energy sloshing (beating).
title("Spring chain on an incline — coupled oscillators");
canvas("16:9");
text(hdr, (cx, 44), "Three masses, two springs — the energy sloshs between the modes"); size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
springchain(sc, (500, 300), 22);
energygraph(sc, (1010, 340), 120);
text(cap, (cx, h - 32), "pull one block and the whole chain rings — normal modes / beating (coupled springs)"); size(cap, 15); color(cap, dim); display(cap);
run(sc, 9);
incline-showcase
One paper page, FOUR live incline problems: a friction ramp, an incline+pulley, a
two-slope wedge, and a spring bumper — revealed one at a time with narration, then all
run in parallel. Real base-manic staging (template("paper") + hidden/show + say
par), not a physics dump.
// ============================================================================
// incline-showcase.manic — four incline problems, one paper page, all live
// ----------------------------------------------------------------------------
// A single annotated lesson that puts ALL FOUR animated incline sims on one
// `template("paper")` page and runs them together — real base-manic composition
// (staged reveal with `hidden`/`show`, `say` narration, per-quadrant labels),
// not a physics dump. Each sim's bare id broadcasts, so `hidden(rp)`/`show(rp)`
// address a whole sim at once. Finishes with all four running in parallel.
// ============================================================================
title("Four inclined-plane problems — one page");
canvas("16:9");
template("paper");
text(hdr, (cx, 34), "Four inclined-plane problems, one page"); color(hdr, fg); size(hdr, 26); bold(hdr); display(hdr); hidden(hdr);
// the four sims (each hidden; the bare id broadcasts to all its parts)
ramp(rp, (110, 340), 28, 5, 0, 45); hidden(rp);
inclinepulley(ip, (690, 340), 30, 3, 2, 42); hidden(ip);
doubleincline(dw, (300, 660), 50, 30, 12, 70, 34); hidden(dw);
inclinebumper(ib, (720, 660), 38, 2, 500, 42); hidden(ib);
// quadrant labels
text(l1, (150, 175), "① friction on a ramp"); color(l1, dim); size(l1, 17); display(l1); hidden(l1);
text(l2, (770, 175), "② incline + pulley"); color(l2, dim); size(l2, 17); display(l2); hidden(l2);
text(l3, (150, 505), "③ two-slope wedge"); color(l3, dim); size(l3, 17); display(l3); hidden(l3);
text(l4, (770, 505), "④ spring bumper"); color(l4, dim); size(l4, 17); display(l4); hidden(l4);
text(cap, (cx, h - 26), ""); color(cap, fg); size(cap, 19); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(cap, "one page, four classic incline problems — all real, pre-simulated physics", 0.4);
wait(0.5);
say(cap, "① a block sliding down a rough ramp", 0.4);
show(rp, 0.5); show(l1, 0.3);
wait(0.5);
say(cap, "② a block on an incline, tied over a pulley to a hanging mass", 0.4);
show(ip, 0.5); show(l2, 0.3);
wait(0.5);
say(cap, "③ two blocks on a wedge, connected over the apex", 0.4);
show(dw, 0.5); show(l3, 0.3);
wait(0.5);
say(cap, "④ a block sliding into a spring bumper at the base", 0.4);
show(ib, 0.5); show(l4, 0.3);
wait(0.6);
say(cap, "…and they all run — same page, same physics engine, four different motions", 0.4);
par {
run(rp, 7);
run(ip, 7);
run(dw, 7);
run(ib, 7);
}
textbook-incline-fbd
A block on an incline as a physics-class FREE-BODY DIAGRAM: the reusable forces(id)
view draws gravity/normal/friction/a vectors on the block, a second panel redraws them
from a point, and template("paper") inks it — then run slides the block (ramp).
// ============================================================================
// textbook-incline-fbd.manic — the free-body diagram, textbook style
// ----------------------------------------------------------------------------
// A block on an incline shown the classic physics-class way: the physical
// picture on the left with the force vectors ON the block (via the reusable
// `forces(id)` view on the `ramp` sim — gravity `mg`, normal `N`, friction `f`,
// and the acceleration `a`), and a free-body-diagram panel on the right drawing
// the same forces from a single point. `template("paper")` inks it; then `run`
// plays the slide with the vectors riding the block.
// ============================================================================
title("Block on an incline — the free-body diagram");
canvas("16:9");
template("paper");
text(hdr, (cx, 42), "Block on an incline — the free-body diagram"); color(hdr, fg); size(hdr, 24); bold(hdr); display(hdr);
// left: the incline sim, inked; its force vectors revealed by forces()
ramp(rp, (170, 470), 30);
outlined(rp.block); outline(rp.block, fg);
// right: the free-body diagram — the same four forces from one point
dot(o, (1000, 330), 6); color(o, fg);
text(fbdT, (1000, 205), "free-body diagram"); color(fbdT, dim); size(fbdT, 16); display(fbdT);
arrow(aN, (1000, 330), (955, 252)); color(aN, lime); stroke(aN, 3);
text(lN, (944, 240), "N"); color(lN, lime); size(lN, 18); display(lN);
arrow(af, (1000, 330), (1078, 285)); color(af, magenta); stroke(af, 3);
text(lf, (1086, 280), "f"); color(lf, magenta); size(lf, 18); display(lf);
arrow(ag, (1000, 330), (1000, 440)); color(ag, blue); stroke(ag, 3);
text(lg, (1010, 448), "mg"); color(lg, blue); size(lg, 18); display(lg);
arrow(aa, (1000, 330), (930, 372)); color(aa, red); stroke(aa, 3);
text(la, (908, 380), "a"); color(la, red); size(la, 18); display(la);
text(cap, (cx, h - 34), ""); color(cap, fg); size(cap, 19); display(cap);
// ============================== SCRIPT ==============================
say(cap, "three forces act on the block: gravity, the normal force, and friction", 0.4);
forces(rp, 0.9);
wait(1.0);
say(cap, "drawn from a single point, they make the free-body diagram →", 0.4);
wait(0.9);
say(cap, "their sum points down the slope, so the block accelerates and slides", 0.4);
run(rp, 5);
pulley-annotated
The Atwood machine elevated with CAMERA work: cam + zoom push in on the two masses
for the imbalance beat and glow the heavier one, a counter ticks the acceleration up,
then it pulls back to release — cinematography instead of section cards.
// ============================================================================
// pulley-annotated.manic — ELEVATE a sim: CAMERA work (cam + zoom)
// ----------------------------------------------------------------------------
// A different elevation flavour from spring-annotated (typewriter) — here the
// camera does the storytelling: `cam` + `zoom` push in on the two masses for
// the "imbalance" beat, glow the heavier one, then pull back for the release.
// A live `counter` ticks the acceleration up. No stage-covering section cards,
// so the motion is always visible. All base manic over the `pulley` parts.
// ============================================================================
title("The Atwood Machine — Why the Heavier Side Falls");
canvas("16:9");
text(hdr, (cx, 40), "The Atwood Machine"); size(hdr, 28); color(hdr, cyan); bold(hdr); display(hdr); hidden(hdr);
// ---- the sim, centred so the camera can push in ----
pulley(pl, (cx, 190), 3, 2);
hidden(pl.wheel); hidden(pl.hub); hidden(pl.ropeL); hidden(pl.ropeR); hidden(pl.mass1); hidden(pl.mass2);
text(wheelL, (cx, 118), "frictionless pulley"); size(wheelL, 17); color(wheelL, dim); display(wheelL); hidden(wheelL);
text(m1L, (cx - 150, 320), "m₁ = 3 kg"); size(m1L, 19); color(m1L, cyan); display(m1L); hidden(m1L);
text(m2L, (cx + 150, 320), "m₂ = 2 kg"); size(m2L, 19); color(m2L, magenta); display(m2L); hidden(m2L);
counter(aC, (cx, 250), 0, 2, "a = ", " m/s²"); size(aC, 26); color(aC, gold); display(aC); hidden(aC); sticky(aC);
// caption is `sticky` — it stays pinned to the screen through the camera push-in
text(cap, (cx, h - 44), ""); size(cap, 21); color(cap, dim); display(cap); sticky(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(cap, "one rope over one frictionless pulley …", 0.4);
show(pl.wheel, 0.4); show(pl.hub, 0.3); pulse(pl.wheel); show(wheelL, 0.4);
wait(0.3);
say(cap, "… a heavier mass on the left, a lighter one on the right", 0.4);
show(pl.ropeL, 0.3); show(pl.mass1, 0.4); pulse(pl.mass1); show(m1L, 0.4);
show(pl.ropeR, 0.3); show(pl.mass2, 0.4); pulse(pl.mass2); show(m2L, 0.4);
wait(0.5);
// ---- CAMERA: push in on the masses for the imbalance beat ----
// cap + aC are sticky, so they stay put on screen while the world zooms
say(cap, "look closely — the net pull favours the heavier side", 0.4);
par { cam((cx, 330), 1.4, smooth); zoom(1.7, 1.4, smooth); }
glow(pl.mass1, 1.9); flash(pl.mass1, gold); pulse(pl.mass1);
wait(1.0);
par { cam((cx, cy), 1.2, smooth); zoom(1, 1.2, smooth); } // pull back out
wait(0.2);
// the acceleration, as a live readout
show(aC, 0.3); to(aC, value, 1.96, 1.0); // a = (m₁−m₂)g/(m₁+m₂)
fade(wheelL, 0.3); fade(m1L, 0.3); fade(m2L, 0.3);
wait(0.5);
say(cap, "release: m₁ sinks, m₂ rises", 0.4);
run(pl, 4);
pulley-paper
The SAME Atwood sim dressed as a textbook figure AND run: template("paper") inks it
automatically, a hatched support ceiling, a forest-green wheel and outlined mass boxes,
a base-manic reveal, then run plays the motion — the paper treatment on a LIVE sim.
// ============================================================================
// pulley-paper.manic — an ANIMATED sim in textbook paper style
// ----------------------------------------------------------------------------
// The Atwood `pulley` sim, dressed as a physics-textbook figure AND run: the
// `template("paper")` page (its palette remap inks the sim automatically), a
// hatched `support` ceiling, a forest-green wheel, outlined mass boxes, and a
// base-manic reveal (`say`/`show`/`draw`/`fade`) — then `run` plays the motion.
// Proof the paper/support treatment composes with a live, animated simulation.
// ============================================================================
title("The Atwood machine — textbook style");
canvas("16:9");
template("paper"); // white page; the theme remaps neon → ink
text(hdr, (cx, 46), "The Atwood machine"); color(hdr, fg); size(hdr, 27); bold(hdr); display(hdr); hidden(hdr);
// hatched ceiling + suspension rope (traced on)
support(ceil, (cx, 108), 320); untraced(ceil);
line(sus, (cx, 108), (cx, 160)); color(sus, fg); stroke(sus, 2); untraced(sus);
// the sim, restyled to textbook ink (parts hidden, revealed in order)
pulley(pl, (cx, 192), 3, 2);
color(pl.wheel, lime); // lime → forest green on the paper palette
color(pl.hub, fg); color(pl.ropeL, fg); color(pl.ropeR, fg);
outlined(pl.mass1); outline(pl.mass1, fg);
outlined(pl.mass2); outline(pl.mass2, fg);
hidden(pl.wheel); hidden(pl.hub); hidden(pl.ropeL); hidden(pl.ropeR); hidden(pl.mass1); hidden(pl.mass2);
// annotations
text(m1L, (cx - 150, 316), "m₁ = 3 kg"); color(m1L, fg); size(m1L, 20); display(m1L); hidden(m1L);
text(m2L, (cx + 150, 316), "m₂ = 2 kg"); color(m2L, fg); size(m2L, 20); display(m2L); hidden(m2L);
text(acc, (cx, 604), "a = (m₁ − m₂)·g / (m₁ + m₂)"); color(acc, fg); size(acc, 22); display(acc); hidden(acc);
text(cap, (cx, h - 40), ""); color(cap, fg); size(cap, 20); display(cap);
// ============================== SCRIPT ==============================
show(hdr, 0.5);
say(cap, "a rope over one frictionless pulley, hung from the ceiling", 0.4);
draw(ceil, 0.4); draw(sus, 0.4); show(pl.wheel, 0.4); show(pl.hub, 0.3);
wait(0.4);
say(cap, "a heavier mass on the left, a lighter one on the right", 0.4);
show(pl.ropeL, 0.3); show(pl.mass1, 0.4); show(m1L, 0.3);
show(pl.ropeR, 0.3); show(pl.mass2, 0.4); show(m2L, 0.3);
wait(0.5);
say(cap, "the system accelerates toward the heavier side", 0.4);
show(acc, 0.5);
wait(0.7);
fade(m1L, 0.3); fade(m2L, 0.3);
say(cap, "release: m₁ sinks, m₂ rises", 0.4);
run(pl, 4);
ramp
A block sliding down an inclined plane with static/kinetic friction — the full force
model. Friction turns motion into heat, so the total-energy line steadily falls
(ramp + energygraph).
// ============================================================================
// ramp.manic — friction on an incline, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `ramp(id, [center], [angle], [mass], [applied], [unit])` — a block sliding
// down an inclined plane. Full force model: gravity along the slope, the normal
// force, and a static→kinetic friction switch. Friction turns mechanical energy
// into heat, so `energygraph`'s total DECAYS as the block slides. PRE-SIMULATED
// with RK4; `run(id,[dur])` replays it.
//
// TO ADAPT: lower the angle below the friction angle and the block won't budge.
// ============================================================================
title("Inclined plane — friction dissipates energy");
canvas("16:9");
text(hdr, (cx, 44), "Block on a ramp — μ friction bleeds the energy away");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);
ramp(rp, (300, 470), 30);
energygraph(rp, (960, 320), 120);
text(cap, (cx, h - 32), "as the block slides down, the gold total energy line falls — friction converts it to heat");
size(cap, 16); color(cap, dim); display(cap);
run(rp, 6);
drop-mass
A mass dropped onto a spring-block STICKS — a perfectly inelastic collision. Watch the
total-energy line step down at impact, then the heavier combined mass oscillate about
a lower equilibrium (dropmass + energygraph).
// ============================================================================
// drop-mass.manic — an inelastic collision, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `dropmass(id, [center], [dropheight], [unit])` — a mass is dropped onto a
// block resting on a spring. It free-falls, STICKS (perfectly inelastic), then
// the heavier combined mass oscillates about a lower equilibrium. The collision
// loses kinetic energy — so `energygraph`'s total STEPS DOWN at impact. Then it
// is conserved again. PRE-SIMULATED with RK4; `run(id,[dur])` replays it.
//
// TO ADAPT: raise dropheight for a faster impact and a bigger energy step.
// ============================================================================
title("Drop mass — an inelastic collision loses energy");
canvas("16:9");
text(hdr, (cx, 44), "Dropped mass sticks — energy is lost in the inelastic collision");
size(hdr, 22); color(hdr, cyan); bold(hdr); display(hdr);
dropmass(dm, (440, 150), 1.2);
energygraph(dm, (960, 330), 120);
text(cap, (cx, h - 32), "watch the gold total energy line step DOWN the instant the magenta mass sticks to the block");
size(cap, 15); color(cap, dim); display(cap);
run(dm, 7);
raft-cm
A person walks back and forth on a floating raft; with no external force the centre of
mass stays fixed, so the raft glides the opposite way — momentum conservation you can
see (raft).
// ============================================================================
// raft-cm.manic — centre of mass, on the physics baseline (Layer 1)
// ----------------------------------------------------------------------------
// `raft(id, [center], [personmass], [raftmass], [unit])` — a person walking on
// a floating raft. With no external horizontal force, momentum is conserved and
// the centre of mass stays FIXED: the raft slides the opposite way by
// −m_person/(m_person+m_raft) of each step. PRE-SIMULATED; `run(id,[dur])`
// replays it. The dashed line marks the (unmoving) centre of mass.
//
// TO ADAPT: make the raft much lighter (raftmass) — it slides much further.
// ============================================================================
title("Raft — walk one way, the raft slides the other");
canvas("16:9");
text(hdr, (cx, 46), "Momentum stays zero, so the centre of mass never moves");
size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);
raft(rf, (cx, 380), 70, 200);
text(cap, (cx, h - 32), "the person walks; the raft glides back — the dashed centre-of-mass line stays put");
size(cap, 16); color(cap, dim); display(cap);
run(rf, 9);
brachistochrone
Four beads race under gravity from A to B down a straight line, a circular arc, a
parabola, and a cycloid. The cycloid — the curve of fastest descent — wins, even
though it dips lower and travels farther (brachistochrone).
// ============================================================================
// brachistochrone.manic — the curve of fastest descent (physics Layer 1)
// ----------------------------------------------------------------------------
// `brachistochrone(id, [center], [unit])` — four beads race under gravity from
// A down to B along four curves: a straight line, a circular arc, a parabola,
// and a CYCLOID. Each is a full RK4 bead-on-wire integration. The cycloid — the
// brachistochrone — wins, even though it dips below and travels farther. The
// steep early drop buys speed that more than pays back the extra distance.
// `run(id,[dur])` replays the race.
//
// TO ADAPT: raise `dur` to watch the finish order in slow motion.
// ============================================================================
title("Brachistochrone — the fastest slide is a cycloid");
canvas("16:9");
text(hdr, (cx, 44), "Which curve is fastest from A to B? Steepness early beats a short path");
size(hdr, 21); color(hdr, cyan); bold(hdr); display(hdr);
brachistochrone(br, (330, 150), 150);
text(leg, (cx, h - 58), "dim: straight cyan: arc gold: parabola magenta: CYCLOID (winner)");
size(leg, 16); color(leg, fg); display(leg);
text(cap, (cx, h - 30), "same start, same finish — the magenta cycloid bead reaches B first");
size(cap, 16); color(cap, dim); display(cap);
run(br, 5);
brachistochrone-annotated
The elevation recipe on a RACE, told with KINETIC TYPOGRAPHY: wordpop pops the
question in, karaoke sweeps a highlight across the four path names as the curves
sketch on, then flash/glow crown the cycloid — a third, distinct elevation style.
// ============================================================================
// brachistochrone-annotated.manic — ELEVATE a sim: KINETIC TYPOGRAPHY
// ----------------------------------------------------------------------------
// A third elevation flavour (see spring-annotated for typewriter, pulley-
// annotated for camera). Here the text itself performs: `caption` + `wordpop`
// pops the question in word-by-word, `caption` + `karaoke` sweeps a highlight
// across the four path names as the curves are sketched, and `flash`/`glow`/
// `pulse` crown the winner — the same recipe on a multi-body RACE, no section
// cards. All base manic over the `brachistochrone` parts.
// ============================================================================
title("Brachistochrone — Shortest Path, or Fastest?");
canvas("16:9");
// ---- the sim (curves untraced to sketch on; beads + markers hidden) ----
brachistochrone(br, (330, 165), 150);
untraced(br.straight); untraced(br.circle); untraced(br.parabola); untraced(br.cycloid);
hidden(br.bead_straight); hidden(br.bead_circle); hidden(br.bead_parabola); hidden(br.bead_cycloid);
hidden(br.markA); hidden(br.labelA); hidden(br.markB); hidden(br.labelB);
// ---- kinetic-typography captions ----
caption(q, "shortest path — or fastest?", (cx, 62), 34, gold);
caption(leg, "straight arc parabola cycloid", (cx, h - 66), 26, dim);
text(cap, (cx, h - 32), ""); size(cap, 19); color(cap, dim); display(cap);
// ============================== SCRIPT ==============================
say(cap, "a bead slides from A down to B under gravity — same start, same finish", 0.4);
show(br.markA, 0.4); show(br.labelA, 0.3); pulse(br.markA);
show(br.markB, 0.4); show(br.labelB, 0.3); pulse(br.markB);
wait(0.4);
// the question pops in, word by word
wordpop(q, 0.18);
wait(0.6);
// sketch the four curves; the legend highlight sweeps across their names
say(cap, "four paths from A to B — the cycloid dips below the line", 0.4);
draw(br.straight, 0.5); draw(br.circle, 0.5); draw(br.parabola, 0.5); draw(br.cycloid, 0.6);
karaoke(leg, 0.5, cyan);
flash(br.cycloid, magenta);
wait(0.5);
// the race
say(cap, "release all four at once …", 0.4);
show(br.bead_straight, 0.3); show(br.bead_circle, 0.3); show(br.bead_parabola, 0.3); show(br.bead_cycloid, 0.3);
run(br, 6);
// crown the winner
say(cap, "the CYCLOID wins — the steep early drop buys speed that repays the longer path", 0.4);
flash(br.cycloid, lime); glow(br.bead_cycloid, 1.9); pulse(br.bead_cycloid);
wait(1.0);
textbook-pulley
A physics-TEXTBOOK figure, manic style: the template("paper") white page, a hatched
support ceiling, a green pulley wheel, and outlined labelled mass boxes — the classic
m over 2m+3m arrangement, all base primitives.
// ============================================================================
// textbook-pulley.manic — a physics-textbook figure, manic style
// ----------------------------------------------------------------------------
// The classic "m over 2m+3m" pulley arrangement, drawn in the flat textbook
// look: the `paper` theme (white page, dark ink), a hatched `support` ceiling,
// a solid green pulley wheel, thin ropes, and outlined labelled mass boxes.
// A STATIC figure — pure base manic (`support`, `circle`, `line`, `rect`, `text`).
// ============================================================================
title("Pulley arrangement — m, 2m, 3m");
canvas("16:9");
template("paper");
// hatched ceiling + the rope suspending the pulley
support(ceil, (cx, 120), 340);
line(rope0, (cx, 120), (cx, 222)); color(rope0, fg); stroke(rope0, 2);
// the green pulley wheel (dark edge + hub)
circle(wheel, (cx, 300), 80); color(wheel, lime); outline(wheel, fg);
circle(hub, (cx, 300), 5); color(hub, fg); filled(hub);
// ropes leave the wheel vertically, left and right
line(ropeL, (560, 300), (560, 456)); color(ropeL, fg); stroke(ropeL, 2);
line(ropeR, (720, 300), (720, 456)); color(ropeR, fg); stroke(ropeR, 2);
// mass on the left: m
rect(mbox, (560, 485), 110, 58); outlined(mbox); outline(mbox, fg); stroke(mbox, 2);
text(ml, (560, 485), "m"); color(ml, fg); size(ml, 26); bold(ml);
// masses on the right: 2m, with 3m hanging below it
rect(m2, (720, 485), 120, 58); outlined(m2); outline(m2, fg); stroke(m2, 2);
text(l2, (720, 485), "2m"); color(l2, fg); size(l2, 26); bold(l2);
line(r23, (720, 514), (720, 596)); color(r23, fg); stroke(r23, 2);
rect(m3, (720, 625), 120, 58); outlined(m3); outline(m3, fg); stroke(m3, 2);
text(l3, (720, 625), "3m"); color(l3, fg); size(l3, 26); bold(l3);
textbook-tension
Another textbook figure: two support ropes at 60°/30° meeting a knot, a string over a
hanging pulley carrying 10 kg with the other end anchored to a hatched floor — support
template("paper")for the flat exam-paper look.
// ============================================================================
// textbook-tension.manic — a physics-textbook figure, manic style
// ----------------------------------------------------------------------------
// Two support ropes at 60° / 30° meet at a knot; a string over a hanging pulley
// carries a 10 kg load on one side and anchors to the floor on the other. Drawn
// in the flat textbook look: `paper` theme, hatched `support` ceiling + floor,
// a green pulley wheel, and outlined labels. A STATIC figure, all base manic.
// ============================================================================
title("Tension in a rope over a pulley (g = 10 m/s²)");
canvas("16:9");
template("paper");
// hatched ceiling
support(ceil, (cx, 110), 700);
// the two support ropes meeting at the knot, with a dashed-style vertical ref
line(t1, (553, 110), (cx, 260)); color(t1, fg); stroke(t1, 2);
line(t2, (900, 110), (cx, 260)); color(t2, fg); stroke(t2, 2);
line(vref, (cx, 110), (cx, 260)); color(vref, dim); stroke(vref, 1);
text(a1, (588, 150), "60°"); color(a1, fg); size(a1, 22);
text(a2, (852, 150), "30°"); color(a2, fg); size(a2, 22);
text(t1l, (556, 210), "T₁"); color(t1l, fg); size(t1l, 24); bold(t1l);
text(t2l, (820, 210), "T₂"); color(t2l, fg); size(t2l, 24); bold(t2l);
// knot → hanging pulley
line(drop, (cx, 260), (cx, 364)); color(drop, fg); stroke(drop, 2);
circle(wheel, (cx, 430), 66); color(wheel, lime); outline(wheel, fg);
circle(hub, (cx, 430), 5); color(hub, fg); filled(hub);
// 10 kg load on the right side of the string
line(ropeM, (706, 430), (706, 520)); color(ropeM, fg); stroke(ropeM, 2);
rect(mbox, (706, 548), 84, 56); outlined(mbox); outline(mbox, fg); stroke(mbox, 2);
text(ml, (792, 548), "10 Kg"); color(ml, fg); size(ml, 22); bold(ml);
// the other side anchored to a hatched floor
line(ropeF, (cx, 496), (cx, 600)); color(ropeF, fg); stroke(ropeF, 2);
support(floor, (cx, 612), 150, "up");