简体   繁体   English

p5.js:如何在不改变背景或使用外部 canvas 的情况下在 canvas 形状上打孔?

[英]p5.js: How to punch a hole into a canvas shape, without changing the background or using an external canvas?

I'm trying to figure out a way to punch holes into a thing, but without the hole also going through whatever is in the background already.我正试图找出一种方法在一个东西上打孔,但如果没有这个洞也穿过背景中的任何东西。

  1. the hole is made of a few arbitrary shapes, and is not a simple path I can use to clip.这个洞是由几个任意形状组成的,并不是我可以用来剪辑的简单路径。
  2. The hole is only punched through the foreground shapes, and not all the way into the background (the background should stay as-is).该孔仅穿过前景形状,而不是一直打入背景(背景应保持原样)。

I figured a way to do this with an external context, and then bringing it in. My questions: is there a way to do it on my default canvas, and avoid the complications that might arise from the external context (extra memory, color differences etc)?我想出了一种使用外部上下文执行此操作的方法,然后将其引入。我的问题:有没有办法在我的默认 canvas 上执行此操作,并避免外部上下文可能引起的并发症(额外的 memory,颜色差异ETC)?

Here's a working (p5.js) example, which is using a new context:这是一个工作(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>

There is no easy or built in way to do this without the technique you've already discovered.如果没有您已经发现的技术,就没有简单或内置的方法可以做到这一点。 The only alternative would be to implement boolean geometry operations like subtraction and intersection on arbitrary shapes and splines.唯一的选择是实现boolean 几何运算,例如任意形状和样条的减法和相交。 That would allow you to make arbitrary bezier splines that represent the composites of multiple complex shapes and then draw those directly.这将允许您制作任意贝塞尔样条曲线来表示多个复杂形状的组合,然后直接绘制它们。 This approach would have different behavior with regards to stroke than the removal approach.这种方法在中风方面的行为与去除方法不同。

Just FYI, there are also a pair of methods in p5js erase() and noErase() which have a similar behavior to the blendMode(REMOVE) approach.仅供参考,p5js erase()noErase()中还有一对方法与blendMode(REMOVE)方法具有相似的行为。 I don't think there's any technical benefit, but it might be more idiomatic to use them rather than blend mode.我不认为有任何技术上的好处,但使用它们而不是混合模式可能更惯用。

I agree, as Paul(+1) mentions as well, using multiple p5.Graphics instances (external contexts as you call them) is the most straight forward/readable method.我同意,正如 Paul(+1) 所提到的,使用多个 p5.Graphics 实例(你称之为外部上下文)是最直接/可读的方法。

You could explicitly uses p5.Image and mask() , however there are few more operations involved and the could would be a little less readable.您可以显式使用p5.Imagemask() ,但是涉及的操作很少,并且可读性会降低一些。 Here's an example:这是一个例子:

 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>

A (very) hacky workaround would be to do the same thing with one canvas.一个(非常)hacky 的解决方法是用一个 canvas 做同样的事情。 This would leave the areas inside the circles completely transparent so make them appear blue, simply make the background element behind the blue:这将使圆圈内的区域完全透明,使它们显示为蓝色,只需将背景元素置于蓝色后面:

 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>

This might not be flexible enough though.不过,这可能不够灵活。

Now, assuming your foreground is made of the yellow and green shapes and the background is blue, another option would be manually accessing the pixels[] array and updating pixel values.现在,假设您的前景由黄色和绿色形状组成,而背景是蓝色,另一种选择是手动访问pixels[]数组并更新像素值。 In your example the masks are circular so you could check if:在您的示例中,掩码是圆形的,因此您可以检查:

  • the distance between the current pixel and the circle's centre is smaller than the circle's radius: this means the pixel is inside the circle当前像素到圆心的距离小于圆的半径:这意味着像素在圆内
  • also, if the colour inside the circle is a foreground colour (eg green or yellow in your case) If both conditions match then you could replace this pixel with a background colour (blue in your case)此外,如果圆圈内的颜色是前景色(例如,在您的情况下为绿色或黄色),如果两个条件都匹配,那么您可以用背景色替换此像素(在您的情况下为蓝色)

Here's an example of that:这是一个例子:

 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]; } } } }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>

There are a few things to notice here.这里有几点需要注意。 pixels[] is faster than set(x, y, clr) but it means you need to remember a few details: pixels[]set(x, y, clr)快,但这意味着你需要记住一些细节:

  • call loadPixels() before accessing pixels[] to read/populate the array在访问pixels[]之前调用loadPixels()以读取/填充数组
  • make all the pixels changes required (in this case the circle "masks" / pixels inside circle colour replacement)进行所需的所有像素更改(在这种情况下,圆圈“蒙版”/圆圈内的像素颜色替换)
  • call updatePixels() after pixels[] have been updatedpixels[]更新调用updatePixels()

Also notices it takes a bit of time to execute.还注意到执行需要一些时间。 There could be a few speed improvements such as only iterating over the pixels inside the bounding box of the circle before checking distance and checking squared distance instead of dist() , however this would also make the code less readable.可能会有一些速度改进,例如仅在检查距离之前迭代圆的边界框内的像素并检查平方距离而不是dist() ,但这也会降低代码的可读性。

Finally, referencing Paul's idea of boolean geometry operations maybe you could fake some of that by "cutting" circular areas out the rectangles using beginContour() / endContour() .最后,参考 Paul 的 boolean 几何操作的想法,也许您可以通过使用beginContour() / endContour()从矩形“切割”圆形区域来伪造其中的一些。 If you need lower level control for clipping shapes you could use raw clipping functions as you can access p5's CanvasRenderingContext2D via drawingContext .如果您需要对裁剪形状进行较低级别的控制,则可以使用原始裁剪函数,因为您可以通过drawingContext CanvasRenderingContext2D Unfortunately I won't have the time to provide a detailed example.不幸的是,我没有时间提供详细的示例。 Hopefully the ideas and references above can help you figure out what option (or combination or options) might best work for you.希望上面的想法和参考可以帮助您找出最适合您的选项(或组合或选项)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM