Attractor on Infinite Surface

I‘m trying to get an Attractor simulation running on a pseudo infinite surface, but i can‘t get it to work properly.

What i‘m doing is basically set up a normal 1D Attractor simulation (Particles with pos, direction and updating direction with dist to other particles)(already made some Attractors, so that works just fine), with the difference, that i‘m trying to get it to work on a circle for now (circle = infinite 1D line) and later extend this to a sphere to get an infinite 2D surface.

Now i have the problem that i don‘t know how to calculate the velocity for the other direction. I even got some (pretty complex) formula to get the right values in theory, but they don‘t work right. I‘m sure there is a way better way to get this done. So if someone knows, please tell me.

Thanks in advance

For reference, this is one of the 6 Projects started to get something to work (started over 5 times already…maybe more, cause it Never worked out) :

 Particle p,p2;

void setup() {
   size(screen.width, screen.height);
   frameRate(4);
   p = new Particle(0,0);
   p2 = new Particle(3.5,0);
}

void draw() {
   background(0,0,255);
   p.display();
   p.move();
   p.update();
   
   p2.display();
   p2.move();
   p2.update();
   
   
   if (abs(p.pos-p2.pos) <= PI) {
      p.vel += (p2.pos-p.pos)/2000;
      p2.vel += (p.pos-p2.pos)/2000;
   } else {
     p.vel += ((p2.pos-p.pos)-PI)/2000;
     p2.vel += ((p.pos-p2.pos)-PI)/2000;
   }
   
   p.vel *= 0.95;
   p2.vel *= 0.95;
}
class Particle {
   float pos;
   float vel;
   
   Particle(float x, float v) {
      pos = x;
      vel = v;
   }
   
   void display() {
      point(150+sin(pos)*50,100+-cos(pos)*50);
   }
   
   void move() {
      pos += vel;
   }
   
   void update() {
      pos = pos%(2*PI);
   }
}
1 Like

Hi @Lexyth,

If I understand your description correctly you would like to:

  • display particles around a circle/sphere
  • have these particles attracted to an attractor object
  • have these particles follow a circular path when attracted

If so, how about attaching the particles to an anchor located at the center of the circle with a stick.
This wouldn’t differ from a regular attractor simulation and the stick force would be equivalent to a spring force with no stretch.


(Attractor object located at mouse position pulling particles within its radius. While being attracted, particles stay on the circular path)

Example sketch in Python mode

N = 30
rad = 150
theta = TWO_PI / float(N)

attRadius = 80
attFactor = 5

def setup():
    size(800, 800, P2D)
    strokeWeight(8)
    smooth(8)
    
    global particles, center
    
    particles = [Particle(width/2 + cos(i*theta) * rad, height/2 + sin(i*theta) * rad) for i in xrange(N)]
    center = PVector(width/2, height/2)
    
    
def draw():
    background(255)
    
    a = Attractor(mouseX, mouseY)
    a.attract()
    a.render()
    
    for p in particles:
        p.update()
        p.anchor()
        p.render()
    

class Attractor(object):
    def __init__(self, x, y):
        self.pos = PVector(x, y)

    def attract(self):
        for p in particles:
            d = self.pos.dist(p.pos)   
            if d < attRadius:
                dif = self.pos.copy().sub(p.pos)
                f = dif.setMag((1.0 - d / attRadius)).mult(attFactor)
                p.pos.add(f)
                
    def render(self):
        pushStyle()
        stroke(255, 40, 90)
        strokeWeight(1)
        fill(230, 90, 190, 80)
        ellipse(self.pos.x, self.pos.y, attRadius*2, attRadius*2)
        popStyle()



class Particle(object):
    def __init__(self, x, y):
        self.pos = PVector(x, y)
        self.vel = PVector()
        self.acc = PVector()
        
    def update(self):
        self.pos.add(self.vel)
        self.vel.add(self.acc)
        self.acc.mult(0)
        
    def anchor(self):
        dif = center.copy().sub(self.pos) 
        d = dif.mag()
        stretch = (d - rad) #no stretch
        f = dif.setMag(stretch)
        self.pos.add(f)
        
    def render(self):
        point(self.pos.x, self.pos.y)
        pushStyle()
        strokeWeight(.4)
        line(self.pos.x, self.pos.y, center.x, center.y)
        popStyle()
1 Like

That is not quite what i was trying to achieve :sweat_smile:

What i‘d like to have is :

•Have Particles attract each other with force being 1/distance^2 (although simply dist is Fine, since i can just change that accordingly)

•Have the distance of a particle to another be the path on the Circle they are on (the distance being the angle would be a way to get that)

Example :

P1 = 70°, P2 = 90° == distance = 20°
P1 = 10°, P2 = 340° == distance = 30°

That‘s how the distance should be calculated, and i already got that working to get values, but for some reason it‘s not applying in the attraction correctly and the particles just jiggle, move Crazy and stick in some places…

The formula i used for that in my previous attempt was :

Float a = j-i;
Float b = (i-j)-360; //there might‘ve been a - sign before the (), but i can‘t remember right now.
Float c = (i-j)+360;
Float d = min(a,b,c); // the absolute minimum (-10 > 5)

It worked in Desmos, giving me the correct distance, but in Processing that resulted in that jiggling. And i couldn‘t find any other reason for that. Since removing that formula also got rid of the jiggling im sure there wasn‘t another cause for that…


I‘ve redone the Code a bit, but as you can Test, it‘s still moving nonsensically…

Particle p,p2;

void setup() {
   size(screen.width, screen.height);
   p = new Particle(0,0);
   p2 = new Particle(120,0);
}

void draw() {
   background(0,0,255);
   
   ellipse(150,200,90,90);
   
   //p.pos++;
   //p2.pos+= 0.25;
   
   //p.pos = 2 * mouseX-30;
   //p2.pos = mouseY -50;
   
   p.display();
   p2.display();
   p.move();
   p.update();
   p2.update();
   
   
   float d1 = dist(p.pos, 0, p2.pos, 0);
   float d2 = d1 > 180 ? d1 - 360 : d1;
   float d3 = abs(d2);
   float dp1 = d1 < 180 ? (p.pos < p2.pos ? d3 : -d3) : (p.pos < p2.pos ? -d3 : d3);
   p.vel += 1/dp1;
   //p.vel *= 0.95;
   
   //println(d1, d2, d3, dp1);
}
class Particle {
   float pos;
   float vel;
   
   Particle(float x, float v) {
      pos = x;
      vel = v;
   }
   
   void display() {
      point(150+sin(pos/360*2*PI)*50,200-cos(pos/360*2*PI)*50);
   }

   void update() {
      pos = pos%360;
   }

   void move() {
      pos += vel;
   }
}

You say “the Circle they are on” but any two points are potentially both on an infinite number of circles – not just one. Is there just one circle? Are there many?

There‘s uhm… I can‘t really add an image, so i‘ll try my best to make it clear with Text.

You probably know about those Particle Systems with n particles that attract each other. To get one of those is basically what i‘m trying to do, only that i want the System to be infinite (which isn‘t possible).

So the closest i could get would be to have the space in which they move be infinite. Again, not directly possible. But using a sphere as the surface on which the particles move, instead of a plane, then it‘s at least possible to have infinite motion in one direction.

But that‘s way too complicated for now, so i‘m trying to do this in one Dimension lower. Which comes down to the plane(2D) becoming a line(1D) and therefore the sphere(2D) becoming a circle(1D).

And the force with which 2 particles on this Circle surface attract each other should be 1/distance^2.

Now what should happen is basically that, if you have a point at angle 0° and one at angle
90°, they should meet at 45° (and then go further to 90°/0° respectively, slowing down until they stop there, and then reverse that over and over (since there is no friction in this System)).

The calculation for that is pretty easy. The velocity of the particle at 0° should be 1/(angleB-angleA). The velocity for the one at 90° should be 1/(angleA-angleB).

Now the problem arises when one angle is 350° and the other is 10° for example. Then the distance should be 20°, and i used the formula to achieve this in the last Code i sent. But the acceleration is completely wrong for some reason…

You can try it out yourself, to see how the point moves differently from how the values would suggest.

Don‘t know what else to try and that‘s pretty much all i can think of for now…

Help is very much appreciated :wink:

Just to be sure, can you confirm that the following is NOT what you’re trying to achieve:

com-video-to-gif

N = 4
rad = 100
theta = TWO_PI / float(N)
colors = ('#FA5454', '#21D3B6', '#3384FA', '#FFD256')

def setup():
    size(800, 400, P2D)
    strokeWeight(12)
    smooth(8)
    
    global particles, center
    particles = []
    center = PVector(width/2, height/2)
    
    for i in xrange(N//2): # or N for 4 particles
        x = width/2 + cos(theta * i) * rad
        y = height/2 + sin(theta * i) * rad
        p = Particle(x, y, theta * i, i)
        particles.append(p)
        
        
    
def draw():
    background(255)
    
    pushStyle()
    noFill()
    stroke(0)
    strokeWeight(.4)
    ellipse(width>>1, height>>1, rad*2, rad*2)
    popStyle()
    
    for p in particles:
        p.update()
        p.anchor()
        p.attract()
        p.render()

    fill(0)
    text('angle difference %.2f' % abs(degrees(particles[1].theta - particles[0].theta)), 40, 40)
    #saveFrame("movie/pendulum_####.png") 
    
    
class Particle(object):
    def __init__(self, x, y, a, i):
        self.pos = PVector(x, y)
        self.vel = PVector()
        self.acc = PVector()
        self.theta = a
        self.idx = i
        
    def update(self):
        self.pos.add(self.vel)
        self.vel.add(self.acc)
        self.acc.mult(0)

        self.theta = atan2(self.pos.y - center.y, self.pos.x - center.x)
        
    def anchor(self):
        dif = center.copy().sub(self.pos) 
        d = dif.mag()
        stretch = (d - rad) #no stretch
        f = dif.setMag(stretch)
        self.pos.add(f)
        
    def attract(self):
    
        for p in particles:
            if p != self:
                d = sq(p.theta - self.theta) 
                dif = self.pos.copy().sub(p.pos)
                f = dif.setMag(1.0 / d).limit(.1)
                p.acc.add(f)
        
    def render(self):
        stroke(colors[self.idx])
        point(self.pos.x, self.pos.y)
        pushStyle()
        strokeWeight(.4)
        line(self.pos.x, self.pos.y, center.x, center.y)
        fill(0)
        text(self.idx, self.pos.x + 4, self.pos.y + 4)
        popStyle()
1 Like

Oh… that seems pretty close to what i‘m trying to achieve :sweat_smile:

Seems like i misunderstood it before.

When you mentioned the spring and the part with being attracted to one attractor i thought you meant something like points being placed on sticks around a point and them being pulled towards this attractor, and when the force stops, they just spring back spreading out equally…

My bad :sweat_smile:

But this second video looks very much like what i‘m trying to achieve, although i‘ll have to Test it out.

Sadly i can‘t access my PC at the moment, so i‘ll have to change the Code to processing Java and see if i get it to run on my Phone:sweat_smile:. I‘ll Write once i managed to get it done :wink:

Ok, i managed to convert it and it works like in the gif with 2 points. But if i use more (like 64…) something weird happens.

The points start to go away from the side with the highest attraction… and they just go in circles with a constant force, even if they are close to many points, some will just continue moving past, without even slowing down.

You’re right, this is pretty much what I was showing. However the important part is not the image but the logic that I’m trying to convey: particles attached to a center anchor point through sticks.
The 2 sketches are mostly the same except the second one puts an attraction force on each particle instead of on the mouse position. In both cases particles stay on the circle perimeter despite the attraction force pulling them.

I’m still confused when it comes to apply forces to a particles system, you may want the help from someone more experienced with Euler integration.

All I can say is that:

  • the spring force should normally be added to the velocity vector (with a damping) but in this case I added it to the position vector to avoid shakings when particles are trapped in an attraction field. Can’t really explain the logic.

  • I tried to limit the attraction force directly but limiting the acceleration vector instead somewhat solve your issue. Can’t really explain the logic either.

To sum up:
In def attract(self) replace f = dif.setMag(1.0 / d).limit(.1)
by f = dif.setMag(1.0 / d)

and in def upade(self) limit the acceleration: self.vel.add(self.acc.limit(.2))

edit: For 3D you probably want to resort to euclidean distance for simplicity.

add_library('peasycam')

N = 40
radius = 100
colors = ('#FA5454', '#21D3B6', '#3384FA', '#FFD256')

phi = (sqrt(5) + 1) / 2 - 1 #Golden Ratio
theta = phi * TWO_PI #Golden Angle

particles = []
attFactor = 3.1
 
def setup():
    hint(DISABLE_DEPTH_TEST)
    size(800, 800, P3D)
    strokeWeight(12)
    smooth(8)
    
    global center
    
    cam = PeasyCam(this, 300)
    center = PVector()
 
    #Display particles at Fibonacci sphere vertices' locations
    for i in xrange(N):
        lon = (theta * i) / TWO_PI
        lon -= floor(lon) 
        lon *= TWO_PI
        
        if lon > PI: 
            lon -= TWO_PI
            
        lat = asin(-1 + 2 * i / float(N))
        x = cos(lat) * cos(lon) * radius
        y = cos(lat) * sin(lon) * radius
        z = sin(lat) * radius
        particles.append(Particle(x, y, z, i))

def draw():
    background(255)
    
    # Draw sphere
    pushStyle()
    fill(200)
    strokeWeight(.1)
    sphere(radius - 2)
    popStyle()
    
    for p in particles:
        p.update()
        p.anchor()
        p.attract()
        p.render()
        
        
        
class Particle(object):
    def __init__(self, x, y, z, i):
        self.pos = PVector(x, y, z)
        self.vel = PVector()
        self.acc = PVector()
        self.idx = i
        
    def update(self):
        self.pos.add(self.vel)
        self.vel.add(self.acc.limit(.2))
        self.acc.mult(0)
                
    def anchor(self):
        dif = center.copy().sub(self.pos) 
        d = dif.mag()
        stretch = (d - radius) #no stretch
        f = dif.setMag(stretch)
        self.pos.add(f) #should normally be added to "vel" with a damping ("vel.mult(.96)")
        
    def attract(self):
        for p in particles:
            if p != self:
                d = sq(self.pos.dist(p.pos))
                #d = max(.2, d) #Add momentum to the balancing effect (avoid d = 0, leading to strong attraction force)
                dif = self.pos.copy().sub(p.pos)
                f = dif.setMag(1.0 / d).mult(attFactor)
                p.acc.add(f)
            
    def render(self):
        if self.idx <4:
            stroke(colors[self.idx])
        else:
            stroke(0)
        point(self.pos.x, self.pos.y, self.pos.z)
        pushStyle()
        strokeWeight(.4)
        line(self.pos.x, self.pos.y, self.pos.z, center.x, center.y, center.z)
        popStyle()
1 Like

I replaced the limit on velocity with one on acceleration, but it‘s still the same. Btw, i had to translate the Code from Pyton to Java, since i can’t Access my PC and have to do it on my phone where the App only works with Java…

I‘ll just Post how i converted the Code. Maybe i made some mistake i didn‘t notice, that causes this weird motion.

class Particle {
   PVector pos;
   PVector vel;
   PVector acc;
   float theta;
   int idx;
   
   Particle(float x, float y, float a, float i) {
      pos = new PVector(x, y);
      vel = new PVector(0,0);
      acc = new PVector(0,0);
      theta = a;
      idx = i;
   }
   
   void update() {
      pos.add(vel);
      acc.limit(0.2);
      vel.add(acc);
      acc.mult(0);
      theta = atan2(pos.y - center.y, pos.x - center.x);
   }
   
   void anchor() {
      PVector dif = new PVector(center.x - pos.x, center.y - pos.y);
      float d = dif.mag();
      float stretch = (d-rad);
      //PVector f = new PVector(0,0);
      dif.setMag(stretch);
      pos = new PVector(pos.x+dif.x, pos.y+dif.y);
   }
   
   void attract() {
      for (Particle p : particles) {
         if (p != this) {
            float d = (1/sq(p.theta - theta))*100;
            PVector dif = new PVector(pos.x - p.pos.x, pos.y - p.pos.y);
            //PVector f = ;
            //dif.limit(.1);
            dif.setMag(1.0 / d);
            //dif.limit(.1);
            p.acc.set(acc.x+dif.x, acc.y + dif.y);
         }
      }
   }
   
   void render() {
      //stroke(colors[idx]);
      point(pos.x, pos.y);
      pushStyle();
      strokeWeight(.4);
      line(pos.x, pos.y, center.x, center.y);
      fill(0);
      text(idx, pos.x + 4, pos.y + 4);
      popStyle();
   }
}
int N = 64;
float rad = 100;
float theta = 2 * PI / N;
//Color colors = ('#FA5454', '#21D3B6', '#3384FA', '#FFD256');

Particle[] particles;
PVector center;

void setup() {
   size(screen.width, screen.height);
   
   strokeWeight(12);
   smooth(8);
   
   particles = new Particle[N];
   center = new PVector(screen.width/2, screen.height/2);
   
   for (int i = 0; i < N; i++) {
      float x = width/2 + cos(theta * i) * rad;
      float y = height/2 + sin(theta * i) * rad;
      Particle p = new Particle(x, y, theta * i, i);
      //particles.append(p);
      particles[i] = p;
   }
}

void draw() {
   background(255);
   
   pushStyle();
   noFill();
   stroke(0);
   strokeWeight(.4);
   ellipse(screen.width/2, screen.height/2, rad*2, rad*2);
   popStyle();
   
   for (Particle p : particles) {
      p.update();
      p.anchor();
      p.attract();
      p.render();
   }
   
   fill(0);
   text("angle difference %.2f" + abs(degrees(particles[1].theta - particles[0].theta)), 40, 40);
}

As for the 2D version(technically 3D)… i‘d like to get the circlular Version done first, before i go for it :wink:

1 Like

The distance computation is faulty.

Here below a revised version of your code. Hope that helps.

int N = 10;
float rad = 200;
float theta = 2 * PI / N;

Particle[] particles;
PVector center;

void setup() {
   size(800, 800, P2D);
   strokeWeight(12);
   smooth(8);
   
   particles = new Particle[N];
   center = new PVector(width>>1, height>>1);
   
   for (int i = 0; i < N; i++) {
      float x = width/2 + cos(theta * i) * rad;
      float y = height/2 + sin(theta * i) * rad;
      Particle p = new Particle(x, y, theta * i, i);
      particles[i] = p;
   }
}

void draw() {
   background(255);
   
   pushStyle();
   noFill();
   stroke(0);
   strokeWeight(.4);
   ellipse(width>>1, height>>1, rad*2, rad*2);
   popStyle();
   
   for (Particle p : particles) {
      p.update();
      p.anchor();
      p.attract();
      p.render();
   }
   
   
}


class Particle {
   PVector pos;
   PVector vel;
   PVector acc;
   float theta;
   int idx;
   
   Particle(float x, float y, float a, int i) {
      pos = new PVector(x, y);
      vel = new PVector(0,0);
      acc = new PVector(0,0);
      theta = a;
      idx = i;
   }
   
   void update() {
      pos.add(vel);
      vel.add(acc.limit(0.2));
      acc.mult(0);
      theta = atan2(pos.y - center.y, pos.x - center.x);
   }
   
   void anchor() {
      PVector dif = center.copy().sub(pos); 
      float d = dif.mag();
      float stretch = (d-rad);
      dif.setMag(stretch);
      pos.add(dif);
   }
   
   void attract() {
      for (Particle p : particles) {
         if (p != this) {
            float d = sq(p.theta - theta); // d could be replaced by Euclidean distance
            d = max(.1, d); //if momentum needed
            PVector dif = pos.copy().sub(p.pos); 
            dif.setMag(1.0 / d);
            p.acc.add(dif);
         }
      }
   }
   
   void render() {
      point(pos.x, pos.y);
      pushStyle();
      strokeWeight(.4);
      line(pos.x, pos.y, center.x, center.y);
      fill(0);
      text(idx, pos.x + 6, pos.y + 6);
      popStyle();
   }
}
2 Likes

Yes, thank you very much :sweat_smile:

Now it works, although the result is different from what i expected. The particles that just moved in one direction with a constant velocity also don‘t appear anymore.

Also,rather fascinating, with over 50 particles it looks like the particles show the Dzhanibekov effect, although that might just be something different looking similar…

Now, i‘ll try to get a working version in 1 and then 2 dimensions higher.

Thanks again for your help :blush:

@Lexyth My angle-based distance calculation is wrong, apologies.

Same sketch with Euclidean distance, you’ll see that the motion is much more coherent. Also note that if you display particles equidistantly the force will be the same for each particle and as a result none of them will move.

int N = 10;
float rad = 200;
float theta = 2 * PI / N;
int attFactor = 50;

Particle[] particles;
PVector center;

void setup() {
   size(800, 800, P2D);
   strokeWeight(12);
   smooth(8);
   
   particles = new Particle[N-2];
   center = new PVector(width>>1, height>>1);
   
   for (int i = 0; i < N-2; i++) {
      float x = width/2 + cos(theta * i) * rad;
      float y = height/2 + sin(theta * i) * rad;
      Particle p = new Particle(x, y, i);
      particles[i] = p;
   }
}

void draw() {
   background(255);
   
   pushStyle();
   noFill();
   stroke(0);
   strokeWeight(.4);
   ellipse(width>>1, height>>1, rad*2, rad*2);
   popStyle();
   
   for (Particle p : particles) {
      p.update();
      p.anchor();
      p.attract();
      p.render();
   }
   
   
}


class Particle {
   PVector pos;
   PVector vel;
   PVector acc;
   float theta;
   int idx;
   
   Particle(float x, float y, int i) {
      pos = new PVector(x, y);
      vel = new PVector(0,0);
      acc = new PVector(0,0);
      idx = i;
   }
   
   void update() {
      pos.add(vel);
      vel.add(acc.limit(2));
      acc.mult(0);
   }
   
   void anchor() {
      PVector dif = center.copy().sub(pos); 
      float d = dif.mag();
      float stretch = (d-rad);
      dif.setMag(stretch);
      pos.add(dif);
   }
   
   void attract() {
      for (Particle p : particles) {
         if (p != this) {
            float d = sq(pos.dist(p.pos));
            d = max(.1, d); //if momentum needed
            PVector dif = pos.copy().sub(p.pos); 
            dif.setMag(1.0 / d).mult(attFactor);
            p.acc.add(dif);
         }
      }
   }
   
   void render() {
      point(pos.x, pos.y);
      pushStyle();
      strokeWeight(.4);
      line(pos.x, pos.y, center.x, center.y);
      fill(0);
      text(idx, pos.x + 6, pos.y + 6);
      popStyle();
   }
}
2 Likes