Search code examples
performancep5.jsparticles

Improving halftone / particles performance in P5js


I want the dots to react smoothly so I’m wondering if there’s a way to improve performance on this code.

I’m trying to create an isometric grid of dots that serves both as a halftone effect (which I have reached) and a particle system that reacts to mouse location (gravity / repel).

Because it’s supposed to act like a halftone image, the density of the dots should remain rather high. Any idea would be greatly appreciated

let img;
let smallPoint, largePoint;
let res;
let manualBrightness = 6;
let lineLength = 1;
let row;
let gfg;

function preload() {
  img = loadImage('https://i.imgur.com/Jvh1OQm.jpg');
}

function setup() {
  createCanvas(400, 400);
  smallPoint = 4;
  largePoint = 40;
  imageMode(CENTER);
  noStroke();
  background(0);
  img.loadPixels();
  res = 5;

  row = 0;

  gfg = new Array(floor((img.height)/res));
  for (var i = 0; i < gfg.length; i++) {
    gfg[i] = new Array(floor((img.height)/res));
  }

  var h = 0;
  for (var i = 0; i < gfg.length; i++) {
    row++;
    let localI=i*res;
    for (var j = 0; j < gfg[0].length; j++) {
      let localJ = j*res*2*Math.sqrt(3);
      // localJ=localJ+res*2*Math.sqrt(3);
      gfg[i][j] = brightness(img.get(localJ, localI));
      // console.log(gfg[i][j]);
    }
  }
}

function draw() {
  background(0);

  row = 0;
  for (let i = 0; i<gfg.length; i++){
    let localI = i*res;
    row++;

    for (let j = 0; j<gfg[i].length; j++){
      let localJ = j*res*2*Math.sqrt(3);

      if(row%2==0){
        localJ=floor(localJ+res*Math.sqrt(3));

      }
      let pix = gfg[i][j];
      // B = brightness(pix);
      B=pix;
      B=(1/300)*B*manualBrightness;

      fill(255);
      stroke(255);
      strokeWeight(0);
      ellipse(localJ, localI,2,2);
      fill(255);
      let ellipseSize =B*res*(mouseX/width);
      // if(i%8==0 && (j+4)%8==0){
      //   ellipseSize = 4;
      // }
      ellipse(localJ, localI, ellipseSize,ellipseSize);
    }
  }
}
<html>
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.2.0/p5.js" integrity="sha512-cuCpFhuSthtmbmQ5JjvU7msRYynRI67jVHsQhTP8RY+H4BC9qa9kQJeHTomV9/QnOWJbDpLFKdbIHtqTomJJug==" crossorigin="anonymous"></script>
</head>
<body>
<main>
</main>
</body>


Solution

  • Tl;dr: web editor

    When trying to make code more efficient, one of the best things to do is do as little as possible during your loops. In the draw() block, you have a nested for loop. This is actually kind of a double-nested loop because draw() is a loop in itself. That means that at the deepest level of your loops (when you're iterating over j), you have to do those things many times, and every single frame. It is actually possible to reduce the deepest part of your nested loop to only one command: drawing the circle at the proper location and size.

    Some suggestions I'm making here will make your code far less readable. These suggestions are only good for when you need to do things faster, and in order to maintain readability, I'd recommend putting in comments.

    Some examples in your code:

    • for every single circle and every single frame, the computer sets the fill color twice (both times to the same color), it sets the strokeWeight to 0, and the stroke color to 255. None of that is necessary. In fact, that stuff can go into the setup() block because it is the same for every frame.
    • You draw two circles at each point and frame. If the second one is bigger, the first is invisible. You might be surprised at how much work it takes for a computer to draw things on the screen. It's best to minimize that: only draw one circle. You can set the size by using max(), and you can also use circle() instead of ellipse() (I don't know if it's actually any faster to use circle(), but it looks nicer to me):
    circle(localJ, localI, max(2,ellipseSize));
    
    • That brings me to my next point: declaring variables requires work. Don't use more variables than you have to, and try to define them exactly how they will be when you use them. For example, in your code, ellipseSize is the variable that you plug into the circle function, so why not just make it what you want in the first place, without defining B or pix? Neither B nor pix is used to do anything else, so we can just do this:
    let ellipseSize = B*res*(mouseX/width);
    -> remove line 61: 
    let ellipseSize = (1/300)*B*manualBrightness*res*(mouseX/width);
    -> remove line 60:
    let ellipseSize = (1/300)*pix*manualBrightness*res*(mouseX/width);
    -> remove line 58:
    let ellipseSize = (1/300)*gfg[i][j]*manualBrightness*res*(mouseX/width);
    -> rearrange:
    let ellipseSize = gfg[i][j]*((manualBrightness*res)/(width*300))*mouseX;
    
    • From here, there is no need to multiply gfg[i][j] by (manualBrightness*res)/(width*300) every single time. Those values never change. What we can do here is move all that stuff up to the definition of gfg, on line 37:
    gfg[i][j] = brightness(img.get(localJ, localI));
    ->
    gfg[i][j] = brightness(img.get(localJ, localI)) * (manualBrightness*res)/(width*300);
    let ellipseSize = gfg[i][j]*mouseX;
    
    • Now if you look at where ellipseSize is used, you'll see that it's only used once, in one command. That hardly warrants the use of a variable. A rule I like to use is that you only make a variable if you're going to use it more than 3 times. Otherwise, it just takes up memory and time. Let's see if there are any other variables we can get rid of.

    What does row do inside of this loop? After it is incremented, we basically just have row = i+1. Also, the only thing row is used for is to detect parity, which is easily done with i:

    row%2==0
    is the same as
    i%2 == 1
    

    So there's no need to have row appear anywhere in this loop; we can just use i. Speaking of that if statement, we can actually get rid of it if we're careful. First, we can get rid of the floor function in there, it's not really helping anything. Now let's think for a minute... we're adding something extra on when i%2 is 1 (as opposed to 0). That's exactly the same as saying

    localJ = localJ + (i%2)*res*Math.sqrt(3);
    

    no if statement necessary. But if we get rid of the if statement, then we have two lines in a row where we're assigning a value to localJ. We can condense these two rows:

    let localJ = j*res*2*Math.sqrt(3);
    if(i%2==1){
      localJ = floor(localJ+res*Math.sqrt(3));
    }
    -> get rid of if statement
    let localJ = j*res*2*Math.sqrt(3);
    localJ = localJ+(i%2)*res*Math.sqrt(3);
    -> combine these two lines
    let localJ = j*res*2*Math.sqrt(3)+(i%2)*res*Math.sqrt(3);
    -> factor out res*Math.sqrt(3)
    let localJ = (2*j+(i%2))*res*Math.sqrt(3);
    

    But now we have two variables that are only used once in one command. This does not warrant the use of variables:

    let localJ = (2*j+(i%2))*res*Math.sqrt(3);
    let ellipseSize = gfg[i][j]*mouseX;
    circle(localJ, localI, max(2, ellipseSize));
    -> just put the formulas into circle()
    circle((2*j+(i%2))*res*Math.sqrt(3), 
           localI, 
           max(2, gfg[i][j]*mouseX)
          );
    

    Now we've made it so that the deepest section of the nested loop is only one command. But we can do better! The square root function is one of the hardest basic arithmetic functions, and we're doing it over and over here. So let's make a variable!

    let sqrtShortcut;
    ...
    sqrtShortcut = res * Math.sqrt(3);
    ...
    circle((2*j+(i%2))*sqrtShortcut, 
           localI, 
           max(2, gfg[i][j]*mouseX)
          );
    

    I followed the same process with localI. There is one more thing we can do here, but it's less obvious. In JavaScript, there is an array method called .map(). It basically applies a function to each element of an array and returns a new array with the changed values. I won't walk through the application, but it's in the sketch below.

    At this point, there's really nothing more to do, but I got rid of some unused variables and useless commands. At the end of all of that, it runs about 5 times faster than it used to. The web editor is here.