p5.j​​s:如何在不改变背景或使用外部画布的情况下在画布形状上打孔?

Posted

技术标签:

【中文标题】p5.j​​s:如何在不改变背景或使用外部画布的情况下在画布形状上打孔?【英文标题】:p5.js: How to punch a hole into a canvas shape, without changing the background or using an external canvas? 【发布时间】:2022-01-08 03:20:39 【问题描述】:

我正在想办法在一个东西上打洞,但没有洞也穿过背景中的任何东西。

    这个洞是由几个任意形状组成的,不是我可以用来剪辑的简单路径。 仅在前景形状上打孔,而不是一直打到背景中(背景应保持原样)。

我想出了一种方法来使用外部上下文来执行此操作,然后将其引入。 我的问题:有没有办法在我的默认画布上执行此操作,并避免外部环境可能引起的复杂情况(额外的内存、颜色差异等)?

这是一个使用新上下文的工作 (p5.js) 示例:

function setup() 
   createCanvas(600,600);
   background(255, 0, 0);
   noStroke();


function draw() 
  //blue: stuff in the background that should not change
  fill ("blue");
  rect (20,20,500,500);
      
  //draw on external canvas
  pg = createGraphics(600,600);
  //yellow+green foreground shapes
  pg.fill("green");
  pg.rect(100, 100, 200, 200);
  pg.fill("yellow");
  pg.rect(80, 80, 100, 300);
      
  //punch a hole in the shapes
  pg.fill(0, 0, 255);
  pg.blendMode(REMOVE);
  pg.circle(140, 140, 150);
  pg.circle(180, 180, 150);
  //bring in the external canvas with punched shapes
  image(pg, 0, 0);

  noLoop();
html,
body 
  margin: 0;
  padding: 0;

canvas 
  display: block;
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>

【问题讨论】:

【参考方案1】:

如果没有您已经发现的技术,就没有简单或内置的方法可以做到这一点。唯一的选择是在任意形状和样条上实现boolean geometry operations,例如减法和相交。这将允许您制作任意贝塞尔样条曲线来表示多个复杂形状的组合,然后直接绘制它们。这种方法在中风方面的行为与移除方法不同。

仅供参考,p5js erase()noErase() 中还有一对方法与 blendMode(REMOVE) 方法具有相似的行为。我不认为有任何技术上的好处,但使用它们而不是混合模式可能更惯用。

【讨论】:

【参考方案2】:

我同意,正如 Paul(+1) 所提到的,使用多个 p5.Graphics 实例(您称之为外部上下文)是最直接/可读的方法。

您可以显式使用p5.Imagemask(),但是涉及的操作很少,而且可读性会差一些。这是一个例子:

function setup() 
   createCanvas(600,600);
   background(255, 0, 0);
   noStroke();


function draw() 
  //blue: stuff in the background that should not change
  fill ("blue");
  rect (20,20,500,500);
      
  //draw on external canvas
  pg = createGraphics(600,600);
  //yellow+green foreground shapes
  pg.fill("green");
  pg.rect(100, 100, 200, 200);
  pg.fill("yellow");
  pg.rect(80, 80, 100, 300);
      
  //punch a hole in the shapes
  let msk = createGraphics(600, 600);
  msk.background(0);
  msk.erase();
  msk.noStroke();
  msk.circle(140, 140, 150);
  msk.circle(180, 180, 150);
  let mskImage = msk.get();
  
  pgImage = pg.get();
  pgImage.mask(mskImage);
  
  image(pgImage, 0, 0);

  noLoop();
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

一个(非常)hacky 的解决方法是用一张画布做同样的事情。 这将使圆圈内的区域完全透明,因此使它们显示为蓝色,只需将背景元素设置为蓝色即可:

function setup() 
   createCanvas(600,600);
   background(255, 0, 0);
   noStroke();


function draw() 
  //blue: stuff in the background that should not change
  fill ("blue");
  rect (20,20,500,500);
      
  //draw on external canvas
  // pg = createGraphics(600,600);
  //yellow+green foreground shapes
  fill("green");
  rect(100, 100, 200, 200);
  fill("yellow");
  rect(80, 80, 100, 300);
      
  //punch a hole in the shapes
  fill(0, 0, 255);
  blendMode(REMOVE);
  circle(140, 140, 150);
  circle(180, 180, 150);
  //bring in the external canvas with punched shapes
  // image(pg, 0, 0);

  noLoop();
body
  /* make the HTML background match the canvas blue */
  background-color: #00F;
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

但这可能不够灵活。

现在,假设您的前景是由黄色和绿色形状组成,而背景是蓝色,另一种选择是手动访问 pixels[] 数组并更新像素值。在您的示例中,掩码是圆形的,因此您可以检查:

当前像素到圆心的距离小于圆的半径:这意味着像素在圆内 另外,如果圆圈内的颜色是前景色(例如,在您的情况下为绿色或黄色) 如果两个条件都匹配,那么您可以将此像素替换为背景颜色(在您的情况下为蓝色)

这是一个例子:

function setup() 
   createCanvas(600,600);
   pixelDensity(1);
   background(255, 0, 0);
   noStroke();


function draw() 
  //blue: stuff in the background that should not change
  fill ("blue");
  rect (20,20,500,500);
      
  //draw on external canvas
  //yellow+green foreground shapes
  fill("green");
  rect(100, 100, 200, 200);
  fill("yellow");
  rect(80, 80, 100, 300);
      
  //punch a hole in the shapes
  fill(0, 0, 255);
  
  // make pixels available for reading
  loadPixels();
  // apply each circle "mask" / bg color replacement
  //                        yellow       , green       , bg blue to replace fg with
  circleMask(140, 140, 150, [255, 255, 0], [0, 0x80, 0], [0, 0, 255]);
  circleMask(180, 180, 150, [255, 255, 0], [0, 0x80, 0], [0, 0, 255]);
  // once all "masks" are applied, 
  updatePixels();
  
  noLoop();


function circleMask(x, y, radius, fg1, fg2, bg)
  // total number of pixels 
  let np = width * height;
  let np4 = np*4;
  //for each pixel (i = canvas pixel index (taking r,g,b,a order into account)
  // id4 is a quarter of "i"
  for(let i = 0, id4 =0 ; i < np4; i+=4, id4++)
      // compute x from pixel index
      let px = id4 % width;
      // compute y from pixel index
      let py = id4 / width;
      // if we're within the circle
      if(dist(px, py, x, y) < radius / 2)
        // if we've found foreground colours to make transparent 
        // ([0][1][2] = r, g, b)
        if((pixels[i]   == fg1[0] || pixels[i]   == fg2[0]) &&
           (pixels[i+1] == fg1[1] || pixels[i+1] == fg2[1]) &&
           (pixels[i+2] == fg1[2] || pixels[i+2] == fg2[2]))
          // "mask" => replace fg colour matching pixel with bg pixel
          pixels[i]   = bg[0];
          pixels[i+1] = bg[1];
          pixels[i+2] = bg[2];
      
    
  
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"&gt;&lt;/script&gt;

这里有几点需要注意。 pixels[]set(x, y, clr) 快,但这意味着你需要记住一些细节:

调用loadPixels()之前访问pixels[]以读取/填充数组 进行所需的所有像素更改(在这种情况下,圆圈“蒙版”/圆圈内的像素颜色替换) 致电updatePixels() pixels[] 更新后

还注意到执行需要一些时间。 可能会有一些速度改进,例如仅在检查距离和检查平方距离之前迭代圆的边界框内的像素而不是 dist(),但这也会降低代码的可读性。

【讨论】:

感谢所有这些想法。为了简单起见,我的代码示例中的背景是简单的单色,而孔是简单的两圆。实际上,背景是随机颜色和形状的完全混合,我需要打孔的负空间也是如此,所以不能使用您展示的其他解决方法。 乐于助人。如果答案有帮助,请随意投票。在灵活性方面,我不认为你不使用多个 p5.Graphics 实例就可以逃脱:例如一个要被遮罩的前景和一个可以在其中绘制多个遮罩元素的单个遮罩(将遮罩与所有元素一起应用一次)。

以上是关于p5.j​​s:如何在不改变背景或使用外部画布的情况下在画布形状上打孔?的主要内容,如果未能解决你的问题,请参考以下文章

p5.j​​s 中垂直居中的文本未正确对齐

p5.j​​s 时间问题

html p5.j​​s + p5.dom.js + clmtracker.js

html p5.j​​s中的颜色

html p5.j​​s中的基本形状

html p5.j​​s中的基本循环