How to make particles fly from one word's shape to the next word? (swarm)

Hello,
I try to move the particles from the shape of a String to the next one…
I guess I should remove a part of the particles and create new ones for the new String but I don’t manage to figure out how?!
Thank you very much for your help in advance.
Best wishes,
L

<!-- 
String[] wordsList= new String [4];
PGraphics pg;
Particle[]particles;
PFont pf;
int index=0; // for wordsList
float x = 0;
float y = 0;
int lastWordTime; // for timer
int tileGap =5;
int tileSize = 5;
int gridHorizontal = 150;
int gridVertical = 150;
int particleNum = 0;
color textCol = color(0);
int nbParts = 0;
int nbPartsPerLetter = 200;
float startTime = 0;
float timeT = 0;

void setup() {
  size(500, 500);
  startTime = millis();
  // frameRate(15);
  background(0);
  pf = createFont("ARIAL.TTF", 100);
  wordsList[0] =("Hello");
  wordsList[1] = ("You");
  wordsList[2] = ("There");
  wordsList[3] = ("Here");

  nextString(wordsList[index]);
}

void draw() {
  background(0);
  translate(10, 100);
  timeT= millis()-startTime;
  for (int i= 0; i<nbParts; i++) {
    particles[i].display();
    particles[i].update();
  }

  if (millis()-startTime>2000) {
    index++;
    nextString(wordsList[index%4]);
    startTime = millis();
  }
  if (index>=wordsList.length) {
    index = 0;
  }
}//func

void nextString(String word) {

  nbParts = nbPartsPerLetter*word.length();
  particles = new Particle [nbParts];

  pg = createGraphics(width, height);
  pg.beginDraw();
  pg.textSize(140);
  pg.textLeading(100);
  pg.textAlign(LEFT, TOP);
  pg.fill(textCol);
  pg.text(word, 100, 100);
  pg.endDraw();
  pg.loadPixels();

  for (int i = 0; i<nbParts; i++) {
    particles[i] = new Particle();
    int particleCount = particles.length;
    int particleIndex = 0;
    if (pg.pixels[int(width*y+x)] !=0) {

      Particle newParticle;
      if (particleIndex<particleCount) {
        newParticle = particles[i];
        particleIndex+=1;
      } else {
        newParticle = new Particle();
        newParticle.pos.x = random(width);
        newParticle.pos.y = random(height);

        //particles = (Particle[])append(particles, newParticle);
      }
      newParticle.target.x = x;
      newParticle.target.y = y;
    }
  }
  //pg.updatePixels();
}

/*void mousePressed() {
 if (mouseButton == LEFT) {
 index += 1;
 if (index>=wordsList.length) {
 index = 0;
 }
 nextString(wordsList[index]);
 }
 }*/


class Particle {
  PVector pos = new PVector(0, 0);
  PVector origin = new PVector(0, 0);
  PVector  target = new PVector(0, 0);
  PVector acc = new PVector(0, 0);
  PVector vel = new PVector(0, 0);

  boolean found = false;
  color col = color(255);
  float rad = random(10);
  float newX, newY;
  int MARGIN = 0;
  float maxForce = 0.1;
  float maxSpeed = 4;
  float closeEnough = 50;

  Particle() {

    while (!found) {
      int x= (int)random(width);
      int y = (int) random(height);
      if (pg.pixels[y*width+x]==textCol) {
        pos = new PVector(x, y);
        origin = pos;
        found = true;
      }
    }
  }

  void update() {

    float proximityMult =1.0;
    float distance = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
    if (distance < closeEnough) {
      proximityMult = distance/closeEnough;
    }
    // Add force towards target
    PVector toTarget = new PVector(this.target.x, this.target.y);
    toTarget.sub(this.pos);
    toTarget.normalize();
    toTarget.mult(this.maxSpeed*proximityMult);

    // Calculate steer force
    PVector steer= new PVector(toTarget.x, toTarget.y);
    steer.sub(this.vel);
    steer.normalize();
    steer.mult(this.maxForce);
    acc.add(steer); //Add steer force to acc

    //Move particle;
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.acc.mult(0);

    /*pos.x+=noise(pos.x*cos(TWO_PI)*random(-10, 10))*random(-15, 15);
     pos.y+=noise(pos.y*sin(TWO_PI)*random(-10, 10))*random(-15, 15);*/
  }

  void home() {
    pos.x = lerp(pos.x, x, 0.05);
    pos.y = lerp(pos.y, y, 0.05);
  }



  void display() {
    pos.add(acc);
    if (pos.x < MARGIN) {
      pos.x = MARGIN;
      acc.x*=-1;
    }
    if (pos.x > width-MARGIN) {
      pos.x = width-MARGIN;
      acc.x*=-1;
    }
    if (pos.y < MARGIN) {
      pos.y = MARGIN;
      acc.y*=-1;
    }
    if (pos.y > height-MARGIN) {
      pos.y = height-MARGIN;
      acc.y*=-1;
    }
    fill(col);
    ellipse(this.pos.x, this.pos.y, this.rad, this.rad);
  }
}


-->
1 Like

Without reading this

Did you see Convert text to PShape? - #4 by glv

Hey Chrisir,
Happy new year and best wishes to you :wink:
Thanks a lot for your answer. In fact the thread you’ve sent me is quite far from my actual problem!
In my sketch now when I press the mouse I switch from one word to another and the particles that form these words are set in the form of a PGraphic. This works well. Now what I try to achieve is that when I switch word I’d like the particles to move to form the actual word to the shape of next word. How can I proceed? To use several simple static arrays, one for each word? Or arrayList to dynamically add and retrive particles according to the words’ length?!! Thanks a lot in advance for your help :))

1 Like

maybe, maybe not

I think you want each pixel inside a letter to fly to a pixel position in the next word.

Did you write the method update inside the class?

I don’t fully understand it (so forgive when I write anything stupid), but it is a kind of flying from origin to target (using pos, which are maybe the positions in between. Like easing).

Considering this, I suggest, you analyze TWO images and when building the array, tell each particle

  • its origin (from word 1) AND
  • its target (from word 2)

Then wait several millis (show particles) and then use update to let it start to fly (update and display).

  • When ALL particles have reached their target, make the current pos into origin and fill the target with data from next word. repeat.

Demonstration

Here is a quick and dirty demonstration with easing (and 2 pgs)

String[] wordsList= new String [4];

PGraphics pg;
PGraphics pg2;

Particle[] particles;
PFont pf;
int index=0; // for wordsList
float x = 0;
float y = 0;
int lastWordTime; // for timer
int tileGap =5;
int tileSize = 5;
int gridHorizontal = 150;
int gridVertical = 150;
int particleNum = 0;
color textCol = color(0);
int nbParts = 0;
final int nbPartsPerLetter = 200;
float startTime = 0;
float timeT = 0;

// -------------------------------------------------------------------------------------------

void setup() {
  size(500, 500);
  startTime = millis();
  // frameRate(15);
  background(0);
  pf = createFont("ARIAL.TTF", 100);
  wordsList[0] =("Hello");
  wordsList[1] = ("You");
  wordsList[2] = ("There");
  wordsList[3] = ("Here");

  nextString(wordsList[index]);
}

void draw() {
  background(0);

  // translate(10, 100);
  timeT = millis()-startTime;

  for (int i= 0; i<nbParts; i++) {
    particles[i].display();
    particles[i].update();
  }

  /*
  if (millis()-startTime>2000) {
   index++;
   nextString(wordsList[index%4]);
   startTime = millis();
   }
   if (index>=wordsList.length) {
   index = 0;
   }*/
}//func

void nextString(String word) {

  nbParts = nbPartsPerLetter*word.length();
  particles = new Particle [nbParts];

  pg = createGraphics(width, height);
  pg.beginDraw();
  pg.textSize(140);
  pg.textLeading(100);
  pg.textAlign(LEFT, TOP);
  pg.fill(textCol);
  pg.text(word, 100, 100);
  pg.endDraw();
  pg.loadPixels();

  pg2 = createGraphics(width, height);
  pg2.beginDraw();
  pg2.textSize(140);
  pg2.textLeading(100);
  pg2.textAlign(LEFT, TOP);
  pg2.fill(textCol);
  pg2.text(wordsList[1], 100, 100);
  pg2.endDraw();
  pg2.loadPixels();

  for (int i = 0; i<nbParts; i++) {
    particles[i] = new Particle();
    int particleCount = particles.length;
    int particleIndex = 0;
    if (pg.pixels[int(width*y+x)] !=0) {
      // Particle newParticle = new Particle();
      if (particleIndex<particleCount) {
        //newParticle = particles[i].copy(); // ??? 
        //newParticle.found=false; 
        //particleIndex+=1;
      } else {
        //newParticle = new Particle();
        particles[i] .origin.x = random(width);
        particles[i] .origin.y = random(height);

        //particles = (Particle[])append(particles, newParticle);
      }
    }
    //particles[i] .target.x = random (100, 200); // was x  
    //particles[i] .target.y = random (100, 200); // was y
  }//for
  //pg.updatePixels();
}

/*void mousePressed() {
 if (mouseButton == LEFT) {
 index += 1;
 if (index>=wordsList.length) {
 index = 0;
 }
 nextString(wordsList[index]);
 }
 }*/

//==================================================================================================

class Particle {
  PVector pos = new PVector(0, 0);
  PVector origin = new PVector(0, 0);
  PVector target = new PVector(0, 0);

  PVector acc = new PVector(0, 0);
  PVector vel = new PVector(0, 0);

  boolean found = false;
  color col = color(255);
  float rad = random(10);
  float newX, newY;
  int MARGIN = 0;
  float maxForce = 0.1;
  float maxSpeed = 4;
  float closeEnough = 50;

  Particle() {
    while (!found) {
      int x = (int)random(width);
      int y = (int)random(height);
      if (pg.pixels[y*width+x]==textCol) {
        pos = new PVector(x, y);
        origin = pos;
        found = true;
      }
    }

    found = false;
    while (!found) {
      int x = (int)random(width);
      int y = (int)random(height);
      if (pg2.pixels[y*width+x]==textCol) {
        target = new PVector(x, y);
        // origin = pos;
        found = true;
      }
    }
  }

  void update() {
    float easing=0.0091; 
    pos.x += (target.x-pos.x) * easing; 
    pos.y += (target.y-pos.y) * easing;
  }

  void display() {
    fill(col);
    ellipse(pos.x, pos.y, rad, rad);
  }
}//class 
//

1 Like

I used to solve things like this by populating a list of targets from the pixel data. If the pixel is black — it’s a valid target for particles to fly. Then you just create new PGraphics, analyze pixels and re-populate the list of targets every time you want to switch the word.

2 Likes

Dear @Chrisir,
You are ze Maaaaaaaster !! Thank you very much a thousand times. Simple arrays look lighter for the CPU than arrayList, or am I wrong?! I have an arrayList version too which is dynamic with the add() and remove() methods, but yours is super great! Thank you very much for your great help as usual ;)) Best wishes, @lolonulu

1 Like

Thank you very much @grshch for your help you put me on the right track :wink:
best wishes,
@lolonulu

1 Like

I worked on this too

  • So when you show your code, I will show mine…
1 Like

With pleasure :wink:

Laurent Mareschal

https://www.instagram.com/laurent.mareschal/
P +33 (0)6.65.62.44.59

----- Mail d’origine -----

1 Like

fantastique!

Merci beaucoup!

Chrisir

1 Like

I meant the code but never mind

here is mine:


String[] wordsList= new String [4];

PGraphics pgOfWord_1;
PGraphics pgOfWord_2;

Particle[] particles;

PFont pf;
int index=0; // for wordsList

color textCol = color(0);
int nbParts = 0;
final int nbPartsPerLetter = 200;

float startTime = 0;
float timeT = 0;

boolean firstTime=true; 

// ----------------------------------------------------------------------------------------

void setup() {
  size(500, 500);

  startTime = millis();
  background(0);
  pf = createFont("ARIAL.TTF", 100);
  wordsList[0] =("Hello");
  wordsList[1] = ("You");
  wordsList[2] = ("There");
  wordsList[3] = ("Here");

  nextString();
}

void draw() {
  background(0);

  timeT = millis()-startTime;

  // wait 2 secs before start 
  if (firstTime) {
    if (millis()-startTime>2000) {
      firstTime = false; 
      startTime = millis();
    }
  }

  for (int i= 0; i<nbParts; i++) {
    particles[i].display();
    if (! firstTime) {
      particles[i].update();// wait 2 secs before start
    }
  }

  // next word
  if (millis()-startTime>12000) {
    index++;
    if (index>=wordsList.length) {
      index = 0;
    }
    nextString();
    startTime = millis();
  }
}//func

void nextString() {

  nbParts = nbPartsPerLetter * wordsList[index].length();
  particles = new Particle [nbParts];

  pgOfWord_1 = createGraphics(width, height);
  pgOfWord_1.beginDraw();
  pgOfWord_1.textSize(140);
  pgOfWord_1.textLeading(100);
  pgOfWord_1.textAlign(LEFT, TOP);
  pgOfWord_1.fill(textCol);
  pgOfWord_1.text(wordsList[index], 100, 100);
  pgOfWord_1.endDraw();
  pgOfWord_1.loadPixels();

  pgOfWord_2 = createGraphics(width, height);
  pgOfWord_2.beginDraw();
  pgOfWord_2.textSize(140);
  pgOfWord_2.textLeading(100);
  pgOfWord_2.textAlign(LEFT, TOP);
  pgOfWord_2.fill(textCol);
  int index2 = index+1; 
  if (index2>wordsList.length-1) {
    index2=0;
  }
  pgOfWord_2.text(wordsList[index2], 100, 100);
  pgOfWord_2.endDraw();
  pgOfWord_2.loadPixels();

  for (int i = 0; i<nbParts; i++) {
    particles[i] = new Particle();
  }//for
}//func

// ==================================================================================================

class Particle {
  // one dot 

  PVector pos = new PVector(0, 0);
  PVector origin = new PVector(0, 0);
  PVector target = new PVector(0, 0);

  boolean found = false;
  color col = color(random(256), random(256), random(256)); // color(255);
  float rad = random(2, 11); // 0,10 or 2,10 or 2,22

  boolean finished = false ; 

  // constr
  Particle() {
    // search pos
    while (!found) {
      int x = (int)random(width);
      int y = (int)random(height);
      if (pgOfWord_1.pixels[y*width+x]==textCol) {
        pos = new PVector(x, y);
        origin = pos.copy();
        found = true;
      }
    }

    // search target 
    found = false;
    while (!found) {
      int x = (int)random(width);
      int y = (int)random(height);
      if (pgOfWord_2.pixels[y*width+x]==textCol) {
        target = new PVector(x, y);
        found = true;
      }
    }
  }// constr ----

  void update() {
    float easing=0.0091; 
    pos.x += (target.x-pos.x) * easing; 
    pos.y += (target.y-pos.y) * easing;

    // stop
    if (target.dist(pos) < 1) {
      pos = target.copy();
      finished=true;
    }
  }

  void display() {
    fill(col);
    ellipse(pos.x, pos.y, rad, rad);
  }
  //
}//class 
//

1 Like

Thank you so much dear @Chrisir I’ll post mine tomorrow with the arrayList version…

Laurent Mareschal

https://www.instagram.com/laurent.mareschal/
P +33 (0)6.65.62.44.59

----- Mail d’origine -----

1 Like

Dear Chrisir,
Here is a version with arraylists (I did with the help of a friend). It gives the opportunity to have dynamic arrays to add and suppress particles on the way. I also play with steering force to give the particles an organic/elastic effect thanks to D. Shiffman (The Nature of code)
Thank you very very much for taking some precious time to work on the simple arrays version since I was really stuck there and wanted to have both version to understand fully the differences and advantges of each one in a more complex sketch than the usual example.
It is so helpfull. I hope it will help others too.
Thank you Master Chrisir as usual :wink:
Have a good day,

Laurent

ArrayList<String> wordsList = new ArrayList<String>();
ArrayList<Particle>particles = new ArrayList<Particle>();
int index=0; // for wordsList 
int pixelSteps = 5;
float startTime = 0; // for timer
float timeT = 0;
String fontName = "Arial Bold";

void setup() {
  size(500, 500);
  startTime = millis();
  // frameRate(15);
  background(0);
  // PFont pf = createFont("ARIAL.TTF", 100);
  wordsList.add("Hello");
  wordsList.add("You");
  wordsList.add ("There");
  wordsList.add ("Here");

  nextString(wordsList.get(index));
}

void draw() {
  background(0);
  timeT= millis()-startTime;
  for (int x= particles.size()-1; x>-1; x--) {
    Particle particle = particles.get(x);
    particle.display();
    particle.update();

// If particles are off the screen get rid of them
    if (particle.isKilled) {
      if (particle.pos.x < 0 || particle.pos.x > width || particle.pos.y < 0 || particle.pos.y > height) {
        particles.remove(particle);
      }
    }
  }

  /* if (millis()-startTime>15000) {
   index++;
   nextString(wordsList[index%4]);
   startTime = millis();
   }
   if (index>=wordsList.length) {
   index = 0;
   }*/
}//func

void nextString(String word) {

  PGraphics pg = createGraphics(width, height);
  pg.beginDraw();
  pg.textSize(140);
  pg.textLeading(100);
  pg.textAlign(CENTER);
  PFont font = createFont(fontName, 100);
  pg.textFont(font);
  pg.fill(0);
  pg.text(word, width/2, height/2);
  pg.endDraw();
  pg.loadPixels();

  int particleCount = particles.size();
  int particleIndex = 0;

  // Create a list of random coordinates
  ArrayList<Integer> locsIndexes = new ArrayList<Integer>();
  for (int i = 0; i <(width*height)-1; i+=pixelSteps) { // each 5 pixels
    locsIndexes.add(i); // fill the arrayList
  }

  for (int i = 0; i < locsIndexes.size (); i++) {
    // Pick up a random coordinate for a more 'organic' effect
    int randomIndex = (int)random(0, locsIndexes.size());
    int locIndex = locsIndexes.get(randomIndex);
    locsIndexes.remove(randomIndex);

    // if pixel is not blanck
    if (pg.pixels[locIndex] !=0) {
      int x = locIndex % width;
      int y = locIndex / width;
      
      // Create a new Particle
      Particle newPart;
      //Use the existing particles on the screen
      if (particleIndex < particleCount) {
        newPart = particles.get(particleIndex);//Select the existing parts
        newPart.isKilled = false;
        particleIndex+=1; // add more
      } else {
        // If next word exceed actual particleCount declare a new particle to add more
        newPart = new Particle();
        // Location where to create the new particles
        PVector randomPos = genRandomPos(width/2, height/2, (width+height)/2);
        newPart.pos.x = randomPos.x;
        newPart.pos.y = randomPos.y;
        // Change speed and force to random ones
        newPart.maxSpeed = random(2.0, 4.0);
        newPart.maxForce = newPart.maxSpeed*0.015;
        // Add new particles to existing ones
        particles.add(newPart);
      }
      // Pass the target location
      newPart.target.x = x;
      newPart.target.y = y;
    }
  }
  pg.updatePixels();
  // get rid of the extra particles if a word is shorter after a long one...
  if (particleIndex < particleCount) {
    for (int i = particleIndex; i<particleCount; i++) {
      Particle particle = particles.get(i);
      particle.kill();
    }
  }
}

//Generate a random position for an organic/dynamic effect
PVector genRandomPos(int x, int y, float mag) {
  PVector randomD = new PVector(random(0, width), random(0, width));
  PVector pos = new PVector(x, y);
  pos.sub(randomD);
  pos.normalize();
  pos.mult(mag);
  pos.add(x, y);

  return pos;
}

// Change index word with mouse click
void mousePressed() {
  if (mouseButton == LEFT) {
    index += 1;
    if (index>=wordsList.size()-1) {
      index = 0;
    }
    nextString(wordsList.get(index));
  }
}

//////////////////////////////////////////////////////////////////////PARTICLE CLASS//////////////////////////////////////////////////////////////////

class Particle {
  PVector pos = new PVector(0, 0);
  PVector origin = new PVector(0, 0);
  PVector  target = new PVector(0, 0);
  PVector acc = new PVector(0, 0);
  PVector vel = new PVector(0, 0);

  boolean found = false;
  color col = color(255, 255, 255, (int)random(50, 150));
  float rad = random(10);
  float newX, newY;
  int MARGIN = 0;
  float maxForce = 0.1;
  float maxSpeed = 4;
  float closeEnough = 50;
  boolean isKilled = false;
  color textCol = 0;

  Particle() {
    // No need here wit the nextString method
    /*while (!found) {
     int x= (int)random(width);
     int y = (int) random(height);
     if (pg.pixels[y*width+x]==textCol) {
     pos = new PVector(x, y);
     origin = pos;
     found = true;
     }
     }*/
  }

  void update() {

    // After the steering method by D. Shiffmann
    float proximityMult = 1.0;
    float distance = dist(this.pos.x, this.pos.y, this.target.x, this.target.y);
    if (distance < this.closeEnough) {
      proximityMult = distance/this.closeEnough;
    }
    // Add force towards target
    PVector toTarget = new PVector(this.target.x, this.target.y);
    toTarget.sub(this.pos);
    toTarget.normalize();
    toTarget.mult(this.maxSpeed*proximityMult);

    // Calculate steer force
    PVector steer= new PVector(toTarget.x, toTarget.y);
    steer.sub(this.vel);
    steer.normalize();
    steer.mult(this.maxForce);
    this.acc.add(steer); //Add steer force to acc

    //Move the particle;
    this.vel.add(this.acc);
    this.pos.add(this.vel);
    this.acc.mult(0);//Stop acceleration

    /*pos.x+=noise(pos.x*cos(TWO_PI)*random(-10, 10))*random(-15, 15);
     pos.y+=noise(pos.y*sin(TWO_PI)*random(-10, 10))*random(-15, 15);*/
  }

  void home() {
    pos.x = lerp(pos.x, origin.x, 0.05);
    pos.y = lerp(pos.y, origin.y, 0.05);
  }
  // Method to get rid of the extra particles when at a random pos
  void kill() {
    if (! this.isKilled ) {
      PVector randomPos= genRandomPos(width/2, height/2, (width+height)/2);
      this.target.x = randomPos.x;
      this.target.y = randomPos.y;
      this.isKilled = true;
    }
  }

  void display() {
    noStroke();
    fill(col);
    rect(this.pos.x, this.pos.y, this.rad, this.rad);
  }
}

1 Like

If I understood correctly, there can be made improvements from a technical point of view.

For example do we make the pg to receive the positions of the letters. But obviously the pg is far too big, since it would be enough to enclose the words (and add the offset later).

Also, we search points with random. Not very fast. Alternatively we could search the points for each word with a for loop (checking all points without any random) in setup() and store the points for look up. Then we could run the random over the list which is much faster. (since we would have an additional PVector ArrayList for each word, this suggests an additional class Word)

Chrisir

1 Like

Dear Chrisir, thank you so much for you precious technical advices no time to code these days. But I will try to improve the scketch later on…Danke schoën !

1 Like