PL. 61 · CURVES / MECHANICS / CHOREOGRAPHY
Three-Body Figure-Eight
Cristopher Moore, 1993 · Chenciner & Montgomery, 2000
OPEN THE LIVE PLATE ▸DEFINITION
ẍᵢ = Σⱼ (xⱼ − xᵢ)/|xⱼ − xᵢ|³ three equal masses, one shared orbit x₁(t) = x₂(t + T/3) = x₃(t + 2T/3)
NOTES
Three equal masses chasing each other around a single figure-eight, each a third of a period behind the next — a choreography, in the technical term the discovery created. Moore found it numerically; Chenciner and Montgomery proved it exists with variational calculus, to considerable astonishment, since almost every three-body arrangement tears itself apart. It has no dials: perturb the initial conditions and the braid dissolves, so the sliders here drive the clock and the comet, not the orbit.
PROVENANCE
- Origin
- C. Moore, "Braids in Classical Dynamics", Physical Review Letters 70, 1993; A. Chenciner & R. Montgomery, Annals of Mathematics 152, 2000; initial conditions as refined numerically by Carles Simó
- Standing
- Public domain — celestial mechanics
- Constants
- One period is integrated once and traced forever
- Source
- doi:10.1103/PhysRevLett.70.3675
HOUDINI · VEX
The same published mathematics as a Detail Wrangle body. Paste it into a Wrangle with Run Over set to Detail; every constant is the published value plus a tweak channel, so Create Spare Parameters gives a slider that starts where the paper does.
// FORMA — PL. 61 · THREE-BODY FIGURE-EIGHT — Cristopher Moore, 1993 · Chenciner & Montgomery, 2000
// ẍᵢ = Σⱼ (xⱼ − xᵢ)/|xⱼ − xᵢ|³
// three equal masses, one shared orbit
// x₁(t) = x₂(t + T/3) = x₃(t + 2T/3)
// Paste into a Detail Wrangle (Run Over: Detail), no inputs needed.
// Written from the published mathematics, not adapted from any code.
// Constants arrive at their published values. Press the node's Create
// Spare Parameters button and every tweak becomes a slider — starting
// at 0, the published figure, and moving in the constant's own units.
// https://forma-gen.com/#plate=threebody
// The plate's own colour: FORMA's CURVES accent as a cosine ramp,
// brightest near t = 0 and t = 1, near-black around t = 0.5.
vector forma_ramp(float t){
return set(
0.11 + 0.1196 * cos(6.28318530718 * (t + 0)),
0.46 + 0.5 * cos(6.28318530718 * (t + 0.05)),
0.2453 + 0.2667 * cos(6.28318530718 * (t + 0.1)));
}
// Simó's initial conditions for the figure-eight, one full period under RK4.
// The curve is shared: bodies 2 and 3 ride the same points a third of a lap
// apart, so one closed polyline is the whole choreography.
// waived: speed, tail — they pace the plate's comet, and geometry has no clock
int forma_n = 3000;
float forma_T = 6.32591398;
function float[] forma_accel(float s0[]){
float d[];
resize(d, 12);
for (int i = 0; i < 3; i++){ d[i * 2] = s0[6 + i * 2]; d[i * 2 + 1] = s0[7 + i * 2]; }
for (int i = 0; i < 3; i++){
float ax = 0.0, ay = 0.0;
for (int j = 0; j < 3; j++){
if (i == j) continue;
float dx = s0[j * 2] - s0[i * 2], dy = s0[j * 2 + 1] - s0[i * 2 + 1];
float r = sqrt(dx * dx + dy * dy);
ax += dx / (r * r * r); ay += dy / (r * r * r);
}
d[6 + i * 2] = ax; d[7 + i * 2] = ay;
}
return d;
}
function float[] forma_step(float a[]; float b[]; float sc){
float o[];
resize(o, 12);
for (int i = 0; i < 12; i++) o[i] = a[i] + b[i] * sc;
return o;
}
float st[] = { 0.97000436, -0.24308753, -0.97000436, 0.24308753, 0.0, 0.0,
0.46620368, 0.43236573, 0.46620368, 0.43236573,
-0.93240737, -0.86473146 };
float dt = forma_T / float(forma_n);
int prim = addprim(0, "polyline");
for (int i = 0; i < forma_n; i++){
float k1[] = forma_accel(st);
float k2[] = forma_accel(forma_step(st, k1, dt / 2.0));
float k3[] = forma_accel(forma_step(st, k2, dt / 2.0));
float k4[] = forma_accel(forma_step(st, k3, dt));
for (int j = 0; j < 12; j++)
st[j] += (k1[j] + 2.0 * k2[j] + 2.0 * k3[j] + k4[j]) * dt / 6.0;
// canvas y runs down; negated so the eight lies as the plate shows it
int pt = addpoint(0, set(st[0], -st[1], 0.0));
// colour sweeps the bright lobe of the ramp around the lap
float u = float(i) / float(forma_n);
setpointattrib(0, "Cd", pt, forma_ramp(0.8 + 0.3 * u));
addvertex(0, prim, pt);
}
AFTER EFFECTS · EXPRESSION
The same published mathematics as a Shape Layer path expression. Paste it onto a Path property; every constant is the published value plus a Slider Control named
// FORMA — PL. 61 · THREE-BODY FIGURE-EIGHT — Cristopher Moore, 1993 · Chenciner & Montgomery, 2000
// ẍᵢ = Σⱼ (xⱼ − xᵢ)/|xⱼ − xᵢ|³
// three equal masses, one shared orbit
// x₁(t) = x₂(t + T/3) = x₃(t + 2T/3)
// After Effects port — paste onto a Shape Layer's Path property
// (Contents › Shape › Path). Written from the published mathematics, not
// adapted from any code. Constants arrive at their published values; add a
// Slider Control (Effect › Expression Controls) named <k>_tweak and that
// constant moves in its own units, starting at 0 — the published figure.
// The plate's comet and its reveal are Trim Paths; the stroke colour is
// FORMA's CURVES accent, #3DFF88. Animation runs on time.
// This plate draws 4 separate paths at its published constants:
// duplicate the group (Contents › Group) that many times and each copy draws
// its own part, read from its position in the layer. A Slider Control named
// "part" on the layer pins one instead.
// https://forma-gen.com/#plate=threebody
// A missing slider reads 0, so a bare paste already draws the figure.
function forma_tweak(n){ try { return effect(n)("Slider"); } catch (e){ return 0; } }
var p_speed = 0.8 + forma_tweak("speed_tweak"); // orbit rate · live 0.2 .. 2
// The frame: the plate's W × H canvas is this comp, with the origin at the
// layer's anchor; canvas y already runs down, as After Effects' does.
var forma_W = thisComp.width, forma_H = thisComp.height, forma_t = time;
var forma_phase = 0.37687092972919345; // this plate's own fixed phase, as the page has it
function forma_pt(x, y){ return [x - forma_W / 2, y - forma_H / 2]; }
function forma_partIndex(){
try { return Math.round(effect("part")("Slider")); } catch (e){}
try { return thisProperty.propertyGroup(3).propertyIndex - 1; } catch (e){ return 0; }
}
var forma_part = forma_partIndex();
// The figure-eight three-body choreography (Moore 1993; Chenciner and
// Montgomery 2000) from Simó's initial conditions, integrated by RK4 over
// one period in 3,000 steps, as the page does — the curve is shared, and
// bodies 2 and 3 ride the same points a third of a lap apart. Part 0 is the
// closed orbit, fitted to 0.86 of the width and 0.7 of the height; parts 1,
// 2 and 3 are the three bodies, each a small circle at its place on the
// orbit at this moment, lapping at 0.045·speed laps a second.
// waived: tail — the comet's length is Trim Paths on the orbit, the page's optics
// parts: 4
var T = 6.32591398, N = 3000, dt = T / N;
var st = [0.97000436, -0.24308753, -0.97000436, 0.24308753, 0, 0,
0.46620368, 0.43236573, 0.46620368, 0.43236573, -0.93240737, -0.86473146];
function forma_f(s0){
var d = new Array(12);
for (var i = 0; i < 3; i++){ d[i * 2] = s0[6 + i * 2]; d[i * 2 + 1] = s0[7 + i * 2]; }
for (var a = 0; a < 3; a++){
var ax = 0, ay = 0;
for (var b = 0; b < 3; b++){
if (a === b) continue;
var dx = s0[b * 2] - s0[a * 2], dy = s0[b * 2 + 1] - s0[a * 2 + 1];
var r = Math.sqrt(dx * dx + dy * dy);
ax += dx / (r * r * r); ay += dy / (r * r * r);
}
d[6 + a * 2] = ax; d[7 + a * 2] = ay;
}
return d;
}
function forma_add(a, b, sc){ var o = new Array(12); for (var i = 0; i < 12; i++) o[i] = a[i] + b[i] * sc; return o; }
var raw = [];
var minx = 1e9, maxx = -1e9, miny = 1e9, maxy = -1e9;
for (var i = 0; i < N; i++){
var k1 = forma_f(st), k2 = forma_f(forma_add(st, k1, dt / 2));
var k3 = forma_f(forma_add(st, k2, dt / 2)), k4 = forma_f(forma_add(st, k3, dt));
var nx = new Array(12);
for (var j = 0; j < 12; j++) nx[j] = st[j] + (k1[j] + 2 * k2[j] + 2 * k3[j] + k4[j]) * dt / 6;
st = nx;
raw.push([st[0], st[1]]);
if (st[0] < minx) minx = st[0]; if (st[0] > maxx) maxx = st[0];
if (st[1] < miny) miny = st[1]; if (st[1] > maxy) maxy = st[1];
}
var sc = Math.min(forma_W * 0.86 / (maxx - minx), forma_H * 0.7 / (maxy - miny));
var ox = (forma_W - (maxx - minx) * sc) / 2 - minx * sc;
var oy = (forma_H - (maxy - miny) * sc) / 2 - miny * sc;
var pts = [];
if (forma_part >= 1 && forma_part <= 3){
var at = (forma_t * 0.045 * p_speed) % 1, k = forma_part - 1;
var q = raw[Math.floor(((at + k / 3) % 1) * (N - 1))];
var bx = ox + q[0] * sc, by = oy + q[1] * sc, br = Math.max(0.8, forma_W / 460) * 2.4;
for (var m = 0; m < 20; m++){
var th = m / 20 * 6.283185307179586;
pts.push(forma_pt(bx + br * Math.cos(th), by + br * Math.sin(th)));
}
createPath(pts, [], [], true);
} else {
for (var n = 0; n < N; n++) pts.push(forma_pt(ox + raw[n][0] * sc, oy + raw[n][1] * sc));
createPath(pts, [], [], true);
}