提高 P5js 中的半色调/粒子性能

Improving halftone / particles performance in P5js

我希望圆点能够顺畅地做出反应,所以我想知道是否有办法提高这段代码的性能。

我正在尝试创建一个等距的点网格,它既可以用作半色调效果(我已经达到),也可以用作对鼠标位置(重力/排斥)做出反应的粒子系统。

因为它应该表现得像半色调图像,所以点的密度应该保持相当高。 任何想法将不胜感激

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;博士:web editor

在尝试提高代码效率时,最好的做法之一就是在循环期间尽可能少地执行代码。在 draw() 块中,您有一个嵌套的 for 循环。这实际上是一种双嵌套循环,因为 draw() 本身就是一个循环。这意味着在循环的最深层(当您遍历 j 时),您必须多次执行这些操作,并且每一帧。实际上可以将嵌套循环的最深部分减少到只有一个命令:在适当的位置和大小绘制圆。

我在这里提出的一些建议会大大降低您的代码的可读性。这些建议仅在您需要更快地做事时有用,为了保持可读性,我建议您发表评论。

您的代码中的一些示例:

  • 对于每一个圆圈和每一帧,计算机将填充颜色设置两次(两次都设置为相同的颜色),将 strokeWeight 设置为 0,将描边颜色设置为 255。None of那是必要的。事实上,这些东西可以进入 setup() 块,因为它对每一帧都是相同的。
  • 你在每个点和帧上画两个圆。如果第二个更大,则第一个是不可见的。您可能会对计算机在屏幕上绘制内容所花费的工作量感到惊讶。最好尽量减少:只画一个圆圈。您可以使用 max() 设置大小,也可以使用 circle() 代替 ellipse()(我不知道使用 circle() 是否真的更快,但是我觉得它更好看):
circle(localJ, localI, max(2,ellipseSize));
  • 这引出了我的下一点:声明变量需要工作。不要使用比你必须的更多的变量,并尝试准确地定义它们,当你使用它们时它们将如何。例如,在您的代码中,ellipseSize 是您插入到 circle 函数中的变量,那么为什么不首先将其设为您想要的,而不定义 B 或 pix? B 和 pix 都没有用来做任何其他事情,所以我们可以这样做:
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] 乘以 (manualBrightness*res)/(width*300)。这些价值观永远不会改变。我们在这里可以做的是将所有内容移至 gfg 的定义,第 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;
  • 现在,如果您查看使用 ellipseSize 的位置,您会发现它只在一个命令中使用了一次。这很难保证使用变量。我喜欢使用的一个规则是,如果你要使用它超过 3 次,你只创建一个变量。否则,它只会占用内存和时间。让我们看看是否有任何其他变量我们可以摆脱。

row 在此循环中做了什么?递增后,我们基本上只有 row = i+1。此外,row 唯一用于检测奇偶校验,这很容易通过 i:

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

所以没有必要让 row 出现在这个循环的任何地方;我们可以只使用 i。 说到那个 if 语句,如果我们小心的话,我们实际上可以去掉它。 首先,我们可以摆脱那里的 floor 功能,这并没有什么帮助。 现在让我们想一想……当 i%2 为 1(而不是 0)时,我们要添加一些额外的东西。这和说

完全一样
localJ = localJ + (i%2)*res*Math.sqrt(3);

不需要 if 语句。但是如果我们去掉 if 语句,那么我们在一行中有两行我们正在为 localJ 赋值。我们可以压缩这两行:

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);

但是现在我们有两个变量在一个命令中只使用一次。这不保证使用变量:

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)
      );

现在我们已经做到了,嵌套循环的最深处只有一个命令。但我们可以做得更好!平方根函数是最难的基本算术函数之一,我们在这里一遍又一遍地做。所以让我们做一个变量!

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

我对 localI 进行了相同的处理。我们还可以在这里做一件事,但不太明显。在JavaScript中,有一个数组方法叫做.map()。它基本上将一个函数应用于数组的每个元素,并 returns 一个具有更改值的新数组。我不会详细介绍该应用程序,但它在下面的草图中。

至此,真的没什么可做的了,但是我摆脱了一些不用的变量和无用的命令。最后,它的运行速度比以前快了大约 5 倍。网络编辑器是 here.