Search code examples
javascriptprocessingp5.js

function .push keep replacing all elements same as last one in array


I'm trying to make trails of moving objects by using vector history array in p5js.
but after push updated vector, all elements in this.history replaced as last one.

I've searched some question here but still can't understand.

let ppp = [];

function setup() {
  createCanvas(400, 400);
  
  for (let i = 0; i < 3; i++) {
    let p = new Particle();
    ppp.push(p);
  }
}

function draw() {
  background(220);
  for (let i = 0; i < ppp.length; i++) {
    ppp[i].display();
    ppp[i].update();
  }
}

function Particle() {
  this.pv = createVector(random(width), random(height));
  this.history = [];

  let rndV = p5.Vector.random2D(); 
  this.spdV = rndV.mult(random(1, 3));
  
  this.update = function() {
    this.pv.add(this.spdV);
    this.history.push(this.pv); // replace all vector element
    
    console.log(this.history);
  }
  
  this.display = function() {
    fill(30);
    ellipse(this.pv.x, this.pv.y, 30);
    
    for (let i = 0; i < this.history.length; i++) {
      let trail = this.history[i];
      ellipse(trail.x, trail.y, 10);
    }
  }
}

or if you think my approach isn't the best, I'll be happy to hear any suggestion^^

Thanks,


Solution

  • This can be a bit misleading in javascript:

    this.history.push(this.pv);
    

    You're pushing a reference to the same this.pv pre-allocated vector

    What you are trying to do is something like:

    this.history.push(this.pv.copy());
    

    Where you are allocating memory for a completely new p5.Vector object with the x,y coordinates copied from this.pv (using the copy() method)

    Demo:

    let ppp = [];
    
    function setup() {
      createCanvas(400, 400);
      
      for (let i = 0; i < 3; i++) {
        let p = new Particle();
        ppp.push(p);
      }
    }
    
    function draw() {
      background(220);
      for (let i = 0; i < ppp.length; i++) {
        ppp[i].display();
        ppp[i].update();
      }
    }
    
    function Particle() {
      this.pv = createVector(random(width), random(height));
      this.history = [];
    
      let rndV = p5.Vector.random2D(); 
      this.spdV = rndV.mult(random(1, 3));
      
      this.update = function() {
        this.pv.add(this.spdV);
        this.history.push(this.pv.copy()); // replace all vector element
        
        //console.log(this.history);
      }
      
      this.display = function() {
        fill(30);
        ellipse(this.pv.x, this.pv.y, 30);
        
        for (let i = 0; i < this.history.length; i++) {
          let trail = this.history[i];
          ellipse(trail.x, trail.y, 10);
        }
      }
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>

    Bare in mind as the sketch runs this will use more and more memory.

    If simply need to render the trails and don't need the vector data for anything else you can simply render into a separate graphics layer (using createGraphics()) immediately which will save memory on the long run:

    let ppp = [];
    let trailsLayer;
    
    function setup() {
      createCanvas(400, 400);
      // make a new graphics layer for trails
      trailsLayer = createGraphics(400, 400);
      trailsLayer.noStroke();
      trailsLayer.fill(0);
      
      for (let i = 0; i < 3; i++) {
        let p = new Particle();
        ppp.push(p);
      }
    }
    
    function draw() {
      background(220);
      // render the trails layer
      image(trailsLayer, 0, 0);
      
      for (let i = 0; i < ppp.length; i++) {
        ppp[i].display();
        ppp[i].update();
      }
    }
    
    function Particle() {
      this.pv = createVector(random(width), random(height));
      
      let rndV = p5.Vector.random2D(); 
      this.spdV = rndV.mult(random(1, 3));
      
      this.update = function() {
        this.pv.add(this.spdV);
        // render trails
        trailsLayer.ellipse(this.pv.x, this.pv.y, 10);
      }
      
      this.display = function() {
        fill(30);
        ellipse(this.pv.x, this.pv.y, 30);
      }
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>

    Update to fade trails you could try something like Moving On Curves example. Notice noStroke(); is called in setup() and

    fill(0, 2);
    rect(0, 0, width, height);
    

    render a faded out (alpha=2) rectangle ?

    You could do something similar:

    let ppp = [];
    let trailsLayer;
    
    function setup() {
      createCanvas(400, 400);
      background(255);
      // make a new graphics layer for trails
      trailsLayer = createGraphics(400, 400);
      trailsLayer.noStroke();
      // set translucent fill for fade effect
      trailsLayer.fill(255, 25);
      
      for (let i = 0; i < 3; i++) {
        let p = new Particle();
        ppp.push(p);
      }
    }
    
    function draw() {
      background(220);
      // fade out trail layer by rendering a faded rectangle each frame 
      trailsLayer.rect(0, 0, width, height);
      // render the trails layer
      image(trailsLayer, 0, 0);
      
      for (let i = 0; i < ppp.length; i++) {
        ppp[i].display();
        ppp[i].update();
      }
    }
    
    function Particle() {
      this.pv = createVector(random(width), random(height));
      
      let rndV = p5.Vector.random2D(); 
      this.spdV = rndV.mult(random(1, 3));
      
      this.update = function() {
        this.pv.add(this.spdV);
        // reset at bounds
        if(this.pv.x > width){
          this.pv.x = 0;
        }
        if(this.pv.y > height){
          this.pv.y = 0;
        }
        if(this.pv.x < 0){
          this.pv.x = width;
        }
        if(this.pv.y < 0){
          this.pv.y = height;
        }
        // render trails
        trailsLayer.push();
        trailsLayer.fill(0);
        trailsLayer.noStroke();
        trailsLayer.ellipse(this.pv.x, this.pv.y, 10);
        trailsLayer.pop();
      }
      
      this.display = function() {
        fill(30);
        ellipse(this.pv.x, this.pv.y, 30);
      }
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>

    For the sake of completeness here's a version using the history vector array, but limiting that to a set size and reusing vectors allocated once (instead making new ones continuously):

    let ppp = [];
    
    function setup() {
      createCanvas(400, 400);
      noStroke();
      
      for (let i = 0; i < 3; i++) {
        let p = new Particle();
        ppp.push(p);
      }
    }
    
    function draw() {
      background(220);
      for (let i = 0; i < ppp.length; i++) {
        ppp[i].display();
        ppp[i].update();
      }
    }
    
    function Particle() {
      this.pv = createVector(random(width), random(height));
      // limit number of history vectors
      this.historySize = 24;
      this.history = new Array(this.historySize);
      // pre-allocate all vectors
      for(let i = 0 ; i < this.historySize; i++){
          this.history[i] = this.pv.copy();
      }
    
      let rndV = p5.Vector.random2D(); 
      this.spdV = rndV.mult(random(1, 6));
      
      this.update = function() {
        this.pv.add(this.spdV);
        this.resetBounds();
        this.updateHistory();
      };
      
      this.updateHistory = function(){
        // shift values back to front by 1 (loop from last to 2nd index)
        for(let i = this.historySize -1; i > 0; i--){
          // copy previous to current values (re-using existing vectors)
          this.history[i].set(this.history[i-1].x, this.history[i-1].y);
        }
        // finally, update the first element
        this.history[0].set(this.pv.x, this.pv.y);
      };
      
      this.resetBounds = function(){
        // reset at bounds
        if(this.pv.x > width){
          this.pv.x = 0;
        }
        if(this.pv.y > height){
          this.pv.y = 0;
        }
        if(this.pv.x < 0){
          this.pv.x = width;
        }
        if(this.pv.y < 0){
          this.pv.y = height;
        }
      };
      
      this.display = function() {
        fill(30);
        ellipse(this.pv.x, this.pv.y, 30);
        
        for (let i = 0; i < this.historySize; i++) {
          let trail = this.history[i];
          // fade trails
          let alpha = map(i, 0, this.historySize -1, 192, 0);
          fill(30, alpha);
          ellipse(trail.x, trail.y, 10);
        }
      };
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>