1

I have created a full demonstration of the problem I'm experiencing below:

const rng = (min, max) => Math.random() * (max - min + 1) + min;

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

ctx.strokeStyle = "#000";
ctx.lineWidth = 4;
ctx.fillStyle = "#ff0000";

function drawCircle(c) {
  ctx.beginPath();
  ctx.arc(c.x, c.y, c.r, 0, 2 * Math.PI);
  ctx.stroke();
  ctx.fill();
}

class Circle {
  constructor(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.vX = 0;
    this.vY = 0;
  }
}

const circles = [];

for (let i = 0; i < 300; i++) {
  circles.push(new Circle(rng(0, canvas.width), rng(0, canvas.height), rng(12, 14)));
}

function processCollision(c1, c2) {
  const deltaX = c2.x - c1.x;
  const deltaY = c2.y - c1.y;

  const sumRadius = c1.r + c2.r;
  const centerDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
  
  if (centerDistance === 0 || centerDistance > sumRadius) { return; } // not colliding

  const circleDistance = centerDistance - sumRadius;

  const aX = deltaX / centerDistance;
  const aY = deltaY / centerDistance;

  const force = 5;

  c1.vX += aX * circleDistance * force;
  c1.vY += aY * circleDistance * force;
}

function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  for (const c of circles) {
    c.vX = (canvas.width / 2) - c.x; // move towards center x
    c.vY = (canvas.height / 2) - c.y; // move towards center y
  }
  
  for (const c1 of circles) {
    for (const c2 of circles) {
      c1 !== c2 && processCollision(c1, c2);
    }
  }
  
  for (const c of circles) {
    c.x += c.vX * (1 / 60);
    c.y += c.vY * (1 / 60);

    drawCircle(c);
  }
}

setInterval(update, 16.6666);
<canvas width="600" height="600" style="border:1px solid #d3d3d3;">

Notice how all the circles gravitate around the center. However, they are all heavily colliding with one another. I would like to modify the processCollision function such that the circles no longer significantly overlap one another and instead are roughly evenly spread around the center point.

I tried increasing the force variable, but unfortunately while this does indeed cause greater spread, it also creates lot of shaky and jerky movement. The solution must be smooth, similar to the example above. I have been messing with this for weeks but unfortunately cannot seem to come to a solution.

Ryan Peschel
  • 11,087
  • 19
  • 74
  • 136
  • You can try to implement something like https://en.wikipedia.org/wiki/Lennard-Jones_potential - traditional attraction force + repulsion force acting only at small distances (with overall minimum energy at sumRadius) – MBo Sep 21 '22 at 05:40
  • @MBo Hello! Hmm, I'm not really sure how this would work. I tried looking up some code examples for it and I found [this relevant answer](https://stackoverflow.com/a/29379415/962155) and actually ran the posted pygame example locally, but it just looked like normal circles just bouncing off of walls. They didn't seem to really attract or repulse each other much. Is that just because their example uses different parameters for variables? If so, what should I tweak to get the results I'm looking for? I'm very unfamiliar with this algorithm. – Ryan Peschel Sep 21 '22 at 12:02
  • Lennard-Jones is just example - you can fit some function F(r) that has very high positive values (repulsion) for small distance and negative values (attraction) for larger distances with min value at sumradius. I tried to do this with your code but haven't got reliable result yet. – MBo Sep 21 '22 at 12:15
  • Ah thanks so much for looking into this! Yeah I have been trying similar tweaks where there is a large repulsion when things are heavily colliding but it also just seems to cause explosions of force and really jarring, jerky movement. I am having so much trouble getting something working that is both performant (simple code because it has to run very frequently) and also smooth. – Ryan Peschel Sep 21 '22 at 12:18

1 Answers1

0

This seems to behave the way you probably want (or close to it)... It uses a control theory model combined with a physics model, and one needs to tweak the constants k0, k1, strength, buffer, step_size...

const rng = (min, max) => Math.random() * (max - min + 1) + min;

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
ctx.fillStyle = '#ff0000';

const k0 = 1.5;
const k1 = 5;

const strength = 1000000;
const buffer = 2;

class Disc {
  constructor(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.vX = 0;
    this.vY = 0;
    return;
  }
  drawDisc(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.fill();
    return;
  }
  addVelocity(step_size) {
    this.x = this.x + step_size * this.vX;
    this.y = this.y + step_size * this.vY;
    return;
  }
  addAcceleration(aX, aY, step_size) {
    this.vX = this.vX + step_size * aX;
    this.vY = this.vY + step_size * aY;
    return;
  }
  applyCentralAcceleration(step_size) {
    const accelX = -k1 * this.vX - k0 * (this.x - canvas.width / 2);
    const accelY = -k1 * this.vY - k0 * (this.y - canvas.height / 2);
    this.addAcceleration(accelX, accelY, step_size);
    return;
  }
  applyInteractionAcceleration(that, step_size) {
    let dX = this.x - that.x;
    let dY = this.y - that.y;
    const dist = dX * dX + dY * dY;
    const magnitude = strength / (dist - (this.r + buffer + that.r) ** 2) ** 2;
    dX = magnitude * dX;
    dY = magnitude * dY;
    this.addAcceleration(dX, dY, step_size);
    return;
  }
}

class System {
  constructor(numDiscs) {
    this.n = numDiscs;
    this.discs = [];
    for (let i = 0; i < numDiscs; i++) {
      this.discs.push(
        new Disc(rng(0, canvas.width), rng(0, canvas.height), rng(6, 7))
      );
    }
    return;
  }
  applyCentralAcceleration(step_size) {
    for (let i = 0; i < this.n; i++) {
      this.discs[i].applyCentralAcceleration(step_size);
    }
  }
  applyInteractionAcceleration(step_size) {
    for (let i = 0; i < this.n; i++) {
      for (let j = 0; j < this.n; j++) {
        if (i === j) {
          continue;
        }
        this.discs[i].applyInteractionAcceleration(this.discs[j], step_size);
      }
    }
  }
  applyVelocity(step_size) {
    for (let i = 0; i < this.n; i++) {
      this.discs[i].addVelocity(step_size);
    }
  }
  updateSystemState(step_size) {
    this.applyCentralAcceleration(step_size);
    this.applyInteractionAcceleration(step_size);
    this.applyVelocity(step_size);
    return;
  }
  drawSystemDiscs() {
    for (let i = 0; i < this.n; i++) {
      this.discs[i].drawDisc(ctx);
    }
  }
}

systemOfDiscs = new System(50);

function update() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  const step_size = 1 / 100;
  systemOfDiscs.updateSystemState(step_size);
  systemOfDiscs.drawSystemDiscs();
  return;
}

setInterval(update, 16.6666);
<canvas width="300" height="300" style="border: 1px solid #d3d3d3"></canvas>
Futurologist
  • 1,874
  • 2
  • 7
  • 9