Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 61, Three-Body Figure-Eight: a still of the mechanics / choreography plate as the atlas renders it, in the curves accent.

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 _tweak, so a bare paste already draws the figure and each slider moves one constant in its own units. Trim Paths is the comet.

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