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>
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:
setup()
block because it is the same for every frame.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));
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;
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;
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.