Skip to content

Many-Body Force ​

A simple many-body simulation: every pair of circles attracts or repels depending on their distance, so they settle into a wobbly swarm. Drag a circle to disturb it. The circles are SVG made with cm.svg and updated with cm.attr. Inspired by Konstantin Makhmutov's Wobbly Swarm, d3-force and Daniel Shiffman's Two Body Attraction.

js
const n = Inputs.range([0, 150], {label: "Circle count", step: 1, value: 100});
js
const size = 640;
const radius = 20 * Math.sqrt(n);
const bodies = Array.from({length: n}, () => ({
  position: cm.vec(Math.random() * size, Math.random() * size),
  velocity: cm.vec(),
  mass: 12 / 50,
}));
let selected = null;

function attract(a, b) {
  const force = cm.vecSub(b.position, a.position);
  const d = Math.max(1, force.mag());
  force.mag(a.mass * b.mass * ((d - radius) / d));
  a.velocity.mult(0.99).add(force.div(a.mass));
}

function tick() {
  for (const a of bodies) for (const b of bodies) if (a !== b) attract(a, b);
  for (const body of bodies) if (body !== selected) body.position.add(body.velocity);
}

const svg = cm.svg`<svg ${{width: size, height: size, viewBox: [0, 0, size, size], style_touch_action: "none"}}>
  <circle ${{
    data: bodies,
    r: 12,
    fill: "black",
    style_cursor: "pointer",
    onpointerdown: (event, node, d) => (selected = d),
  }}/>
</svg>`;
const circles = svg.querySelectorAll("circle");

// The pointer in SVG coordinates, also when the SVG is scaled to fit the page.
const pointer = (event) => [(event.offsetX / svg.clientWidth) * size, (event.offsetY / svg.clientHeight) * size];
const onpointerup = () => (selected = null);

svg.addEventListener("pointermove", (event) => selected && selected.position.set(...pointer(event)));
window.addEventListener("pointerup", onpointerup);
invalidation.then(() => window.removeEventListener("pointerup", onpointerup));

requestAnimationFrame(function frame() {
  tick();
  bodies.forEach((d, i) => {
    cm.attr(circles[i], "cx", d.position.x);
    cm.attr(circles[i], "cy", d.position.y);
    cm.attr(circles[i], "fill", d === selected ? "red" : "black");
  });
  requestAnimationFrame(frame);
});

document.body.append(svg);