Search code examples
javascriptmultidimensional-arraynested-loopsp5.js

Unable to set 2D Javascript array inside nested loop


I am trying to convert a 2D grid of (x, y) points to a sphere in space. I loop through all of the points in the grid and set the value to an object {x: someX, y: someY, z: someZ}. But for some reason, the value I get inside the loop is different from the value outside. Do I have a typo or is this something about JS I don't understand yet? Here is the exact section causing issues:

for (let y = 0; y < GRID_WIDTH; y++) {
  for (let x = 0; x < GRID_WIDTH; x++) {
    points[y][x] = grid2Sphere(x, y); //returns {x: someX, y: someY, z: someZ}. This works.
    console.log(points[y][x]);  // This logs the correct values
  }
  console.log(points[y]); // This is always equal to the row where y = 0. It's as if everything that happened in the inner loop did not get set.
}

For those interested, here is the entire file. I am using p5.js:

let R;
let points;
const GRID_WIDTH = 20;

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  stroke("#000");
  R = 100;
  angleMode(RADIANS);
  points = new Array(GRID_WIDTH).fill(new Array(GRID_WIDTH).fill({}));
  
  // Calculate and draw each point.
  for (let y = 0; y < GRID_WIDTH; y++) {
    for (let x = 0; x < GRID_WIDTH; x++) {
      points[y][x] = Object.assign({}, grid2Sphere(x, y));
      console.log(points[y][x]);
    }
    console.log(points[y]);
  }
  
  drawPoints(points);
}


function draw() {
  noLoop();
}

function drawPoints(pArray) {
  for (let y = 0; y < GRID_WIDTH; y++) {
    for (let x = 0; x < GRID_WIDTH; x++) {
      let p = pArray[y][x];
      point(p.x, p.y, p.z);
    }
  }
}

function grid2Sphere(x, y) {
  var radX = map(x, 0, GRID_WIDTH - 1, -1 * PI * R, PI * R);
  var radY = map(y, 0, GRID_WIDTH - 1, -1 * PI / 2 * R, PI / 2 * R);
  var lon = radX / R;
  var lat = 2 * atan(exp(radY / R)) - PI / 2;
  var x_ = R * cos(lat) * cos(lon);
  var y_ = R * cos(lat) * sin(lon);
  var z_ = R * sin(lat);

  return {x: x_, y: y_, z: z_};
}

Solution

  • I think you're on the right track, the confusion stems from here actually:

    points = new Array(GRID_WIDTH).fill(new Array(GRID_WIDTH).fill({}));
    

    to break it down:

    • new Array(GRID_WIDTH).fill({}) creates an array of empty objects
    • new Array(GRID_WIDTH).fill(new Array(GRID_WIDTH).fill({})); makes a new array that is filled with references (not copies) of the first array (e.g. if the first array of {},{},... was called myData, the nested array would contain: myData,myData,... 20 times)

    You could assign/allocate a new array on each y loop:

    // Calculate and draw each point.
      for (let y = 0; y < GRID_WIDTH; y++) {
        // ensure this is a new array (and not the same reference)
        points[y] = [];
        for (let x = 0; x < GRID_WIDTH; x++) {
          points[y][x] = grid2Sphere(x, y);
          console.log(x, y, points[y][x]);
        }
        console.log(y, points[y]);
      }
    

    Here's a modified version of your code:

    let R;
    let points;
    const GRID_WIDTH = 20;
    
    function setup() {
      createCanvas(windowWidth, windowHeight, WEBGL);
      background(255);
      stroke("#000");
      R = 100;
      points = new Array(GRID_WIDTH).fill(new Array(GRID_WIDTH).fill({}));
      
      // Calculate and draw each point.
      for (let y = 0; y < GRID_WIDTH; y++) {
        // ensure this is a new array (and not the same reference)
        points[y] = [];
        for (let x = 0; x < GRID_WIDTH; x++) {
          points[y][x] = grid2Sphere(x, y);
          console.log(x, y, points[y][x]);
        }
        console.log(y, points[y]);
      }
      
      // move camera for visual debugging purposes only
      camera(300, -100, 0, 0, 0, 0, 0, 1, 0);
      drawPoints(points);
    }
    
    function drawPoints(pArray) {
      for (let y = 0; y < GRID_WIDTH; y++) {
        for (let x = 0; x < GRID_WIDTH; x++) {
          let p = pArray[y][x];
          point(p.x, p.y, p.z);
        }
      }
    }
    
    function grid2Sphere(x, y) {
      var radX = map(x, 0, GRID_WIDTH - 1, -1 * PI * R, PI * R);
      var radY = map(y, 0, GRID_WIDTH - 1, -1 * PI / 2 * R, PI / 2 * R);
      var lon = radX / R;
      var lat = 2 * atan(exp(radY / R)) - PI / 2;
      var x_ = R * cos(lat) * cos(lon);
      var y_ = R * cos(lat) * sin(lon);
      var z_ = R * sin(lat);
    
      return {x: x_, y: y_, z: z_};
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script>

    You could simplify further by just using an empty array that gets populated later (without worrying about filling it ahead of time):

    let R = 100;
    let points;
    const GRID_WIDTH = 20;
    
    function setup() {
      createCanvas(windowWidth, windowHeight, WEBGL);
      background(255);
      stroke("#000");
      points = [];
      
      // Calculate and draw each point.
      for (let y = 0; y < GRID_WIDTH; y++) {
        points[y] = [];
        for (let x = 0; x < GRID_WIDTH; x++) {
          points[y][x] = grid2Sphere(x, y);
          console.log(x, y, points[y][x]);
        }
        console.log(y, points[y]);
      }
      
    }
    
    function draw(){
      background(255);
      orbitControl();
      drawPoints(points);
    }
    
    function drawPoints(pArray) {
      beginShape(POINTS);
      for (let y = 0; y < GRID_WIDTH; y++) {
        for (let x = 0; x < GRID_WIDTH; x++) {
          let p = pArray[y][x];
          vertex(p.x, p.y, p.z);
        }
      }
      endShape();
    }
    
    function grid2Sphere(x, y) {
      var radX = map(x, 0, GRID_WIDTH - 1, -PI * R, PI * R);
      var radY = map(y, 0, GRID_WIDTH - 1, -HALF_PI * R, HALF_PI * R);
      var lon = radX / R;
      var lat = 2 * atan(exp(radY / R)) - PI / 2;
      
      return {
        x: R * cos(lat) * cos(lon),
        y: R * cos(lat) * sin(lon),
        z: R * sin(lat)
      };
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script>

    You can simplify even further by using a flat (1D) array (which you can easily set the size of if you need to):

    let R = 100;
    const GRID_WIDTH = 20;
    const NUM_POINTS = GRID_WIDTH * GRID_WIDTH; 
    let points = new Array(NUM_POINTS);
    
    function setup() {
      createCanvas(windowWidth, windowHeight, WEBGL);
      for(let i = 0; i < NUM_POINTS; i++){
        let x = i % GRID_WIDTH;
        let y = i / GRID_WIDTH; // this is a float (ok for now, but with image indices floor() it)
        points[i] = p5.Vector.fromAngles(map(x, 0, GRID_WIDTH - 1, 0, PI), 
                                         map(y, 0, GRID_WIDTH - 1, 0, TWO_PI), 
                                         R);
      }
    }
    
    function draw(){
      background(255);
      orbitControl();
      drawPoints(points);
    }
    
    function drawPoints(pArray) {
      beginShape(POINTS);
      pArray.forEach(p => vertex(p.x, p.y, p.z));
      endShape();
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script>

    Here, I'm also using beginShape() / endShape() / vertex() which should be more efficient than point() (though for your static image won't make a difference) and using p5.Vector which on top of providing x,y,z properties has a bunch of nice linear algebra utilities (e.g. computing angles between vectors, perpendiculars to vectors, etc.) as well as p5.Vector.fromAngles() which does the spherical to cartesian coordinate conversion for you.

    An even shorter version of the above (though less readible):

    let R = 100;
    const GRID_WIDTH = 20;
    let points = new Array(GRID_WIDTH * GRID_WIDTH).fill();
    
    function setup() {
      createCanvas(windowWidth, windowHeight, WEBGL);
      points.forEach((p,i) => points[i] = p5.Vector.fromAngles((i % GRID_WIDTH) / GRID_WIDTH * PI,
                                                               (i / GRID_WIDTH) / GRID_WIDTH * TWO_PI,                                                               R));
    }
    
    function draw(){
      background(255);
      orbitControl();
      drawPoints(points);
    }
    
    function drawPoints(pArray) {
      beginShape(POINTS);
      pArray.forEach(p => vertex(p.x, p.y, p.z));
      endShape();
    }