Subtracting a curveVertex() hole from a blob

Hello everyone!

I’m trying to make a donut-like shape in Processing using curveVertex(). The outer shape is a blob, and I want to subtract a smaller blob from the middle to create a hole. It works reliably with vertex() but not with curveVertex(). It creates a weird bridge from the outer shape to the inner hole at the seam…

Is there a proper way to make it work? If not directly, what’s the best workaround? Here’s a simplified version of what I’m doing:

void setup() {
  size(800, 800);
}

void draw() {
  background(243, 243, 243);

  fill(0, 0, 0);
  stroke(255, 0, 0);
  strokeWeight(5);

  beginShape();

  curveVertex(470, 250);
  curveVertex(350, 250);
  curveVertex(300, 350);
  curveVertex(320, 480);
  curveVertex(400, 550);
  curveVertex(520, 540);
  curveVertex(600, 460);
  curveVertex(580, 330);
  curveVertex(470, 250);
  curveVertex(350, 250);
  curveVertex(300, 350);

  beginContour();
  curveVertex(390, 430);
  curveVertex(420, 380);
  curveVertex(470, 380);
  curveVertex(510, 430);
  curveVertex(480, 480);
  curveVertex(420, 480);
  curveVertex(390, 430);
  curveVertex(420, 380);
  curveVertex(470, 380);
  endContour();

  endShape(CLOSE);
}

Thanks!

Hello @tareksobh,

Using vertex() worked:

  beginContour();
  
  // Woops!
  //vertex(470, 380);
  //vertex(420, 380);  
  //vertex(420, 430);  
  //vertex(470, 430);
    
  vertex(470, 430);
  vertex(420, 430);
  vertex(420, 380);  
  vertex(470, 380);
  endContour();

You will have to generate enough points for a smooth inner circle.

I was not successful with curveVertex() in my initial efforts.

You can also try:

  • PGraphics / Reference / Processing.org
  • Create the outer shape.
  • Create the inner shape and use blendMode(REPLACE) and fill() with transparent pixels.
  • I tried it with circles and that is what you see in image in upper left.

Reference:
beginContour() / Reference / Processing.org

:)

As a workaround you could try something like this which creates two separate shapes. Using the beginContour()…endContour model with Vertex seems not to work. I changed the winding to be reversed for the inner circle but I doubt that it makes any difference. What seems to be important is the overlap of points 1,2,3, ie they are drawn twice for each circle. You can experiment and see what happens if you don’t do this. I think the reason that it doesn’t work with curveVertex the way it does with Vertex is because of the way the runtime code has been written; I haven’t looked at the runtime to study the source code and will leave that to you if you want to pursue it.

void setup() {
  size(800, 800);
}

void draw() {
  background(209);
  stroke(255, 0, 0);
  strokeWeight(5);

  beginShape();
  fill(0);
  curveVertex(470, 250);
  curveVertex(350, 250);
  curveVertex(300, 350);
  curveVertex(320, 480);
  curveVertex(400, 550);
  curveVertex(520, 540);
  curveVertex(600, 460);
  curveVertex(580, 330);
  curveVertex(470, 250);  // same as pt. 1
  curveVertex(350, 250);  // same as pt. 2
  curveVertex(300, 350);  // same as pt. 3
  endShape(CLOSE);

  beginShape();
  fill(209);
  curveVertex(415, 380);
  curveVertex(470, 380);
  curveVertex(510, 420);
  curveVertex(480, 480);
  curveVertex(420, 480);
  curveVertex(390, 430);
  curveVertex(415, 380); // same as pt. 1
  curveVertex(470, 380); // same as pt. 2
  curveVertex(510, 420); // same as pt. 3
  endShape(CLOSE);
/*
  fill(255);
  stroke(0, 255, 0);
  strokeWeight(2.0);
  // ============================= //
  // outer counterclockwise
  fill(255);
  circle(470, 250, 15);
  fill(0);
  text("1", 467, 255);
  fill(255);
  circle(350, 250, 15);
  fill(0);
  text("2", 347, 255);
  fill(255);
  circle(300, 350, 15);
  fill(0);
  text("3", 297, 355);
  fill(255);
  circle(320, 480, 15);
  fill(0);
  text("4", 317, 485);
  fill(255);
  circle(400, 550, 15);
  fill(0);
  text("5", 397, 555);
  fill(255);
  circle(520, 540, 15);
  fill(0);
  text("6", 517, 545);
  fill(255);
  circle(600, 460, 15);
  fill(0);
  text("7", 597, 465);
  fill(255);
  circle(580, 330, 15);
  fill(0);
  text("8", 577, 335);
  // ============================= //
  // inner clockwise
  fill(255);
  circle(415, 380, 20);
  fill(0);
  text("1", 412, 385);
  fill(255);
  circle(470, 380, 20);
  fill(0);
  text("2", 467, 385);
  fill(255);
  circle(510, 420, 20);
  fill(0);
  text("3", 507, 425);
  fill(255);
  circle(480, 480, 20);
  fill(0);
  text("4", 477, 485);
  fill(255);
  circle(420, 480, 20);
  fill(0);
  text("5", 417, 485);
  fill(255);
  circle(390, 430, 20);
  fill(0);
  text("6", 387, 435);
  fill(255);
  */
}

I left code remmed out which shows the draw order of the points.
Output:

The two shape approach with your original vertices works with a PGraphic with modifications to create a transparent hole.

@tareksobh ,

A snippet to get you started:

  pg.blendMode(REPLACE);
  pg.fill(255, 0); //transparent fill  
  
  pg.beginShape();
  pg.curveVertex(390, 430);
  pg.curveVertex(420, 380);
  pg.curveVertex(470, 380);
  pg.curveVertex(510, 430);
  pg.curveVertex(480, 480);
  pg.curveVertex(420, 480);
  pg.curveVertex(390, 430);
  pg.curveVertex(420, 380);
  pg.curveVertex(470, 380);    
  pg.endShape(CLOSE);
  
  pg.blendMode(BLEND);  // default restored

And you can have fun moving it around with the mouse:

image(pg, mouseX-width/2, mouseY-height/2);

This library is also available and can handle the task:

:)

Hello @svan ,

Thank you for looking into this! Your workaround of drawing the inner blob as a separate shape filled with the same color as the background is a clean fix and would work for most cases. However, I’m working on overlapping shapes with alpha transparencies, so I need genuine vector holes to show the underlying layers.

Hello @glv ,

Thanks for the suggestions! You are completely right that using the standard vertex() would work seamlessly inside contours without bridging. I’m just a bit afraid of getting into manually smoothing the curves. Your suggestion of utilizing a PGraphics buffer alongside blendMode(REPLACE) with a transparent fill is also a fantastic solution! The only limitation for my specific case is that it would indiscriminately punch holes through the many overlapping, semi-transparent shapes I’m using.

Another Workaround

I was able to find a workaround that acts as a pure vector fix. I hope someone finds it useful:

curveVertex() requires looking at four points at a time, and the renderer keeps a hidden memory cache of the previous vertices you drew. The bug occurs because beginContour() doesn’t clear this hidden cache. When the hole begins, the renderer attempts to calculate a smooth curve connecting the final points of the outer shape directly to the initial points of the hole, resulting in the tear/bridge artifact.

I initially found a solution by creating a function that does a cache reset, running it right after finishing the main shape and just before beginContour(). This worked for objects that have a single hole, but I had trouble implementing it cleanly on objects with multiple holes.

So, the workaround I decided to go with is to completely bypass the buggy cache by replacing curveVertex() with bezierVertex(), which is stateless. It does not rely on a hidden memory cache because you provide the specific control points on every single line of code. This forces the renderer to perfectly respect the boundary created by beginContour().

To replicate the exact visual appearance of curveVertex(), we can mathematically convert the splines into Cubic Bezier curves. By finding the distance between the previous point and the next point, and dividing that vector by 6.0, we perfectly mimic Processing’s default curve tightness without triggering the cache glitch.

// Storing the points of the shapes
PVector[] outerShape = {
  new PVector(470, 250), new PVector(350, 250), new PVector(300, 350),
  new PVector(320, 480), new PVector(400, 550), new PVector(520, 540),
  new PVector(600, 460), new PVector(580, 330)
};

PVector[] innerHole = {
  new PVector(390, 430), new PVector(420, 380), new PVector(470, 380),
  new PVector(510, 430), new PVector(480, 480), new PVector(420, 480)
};

void setup() {
  size(800, 800);
}

void draw() {
  background(243, 243, 243);
  fill(0, 0, 0);
  stroke(255, 0, 0);
  strokeWeight(5);

  beginShape();
  drawSmoothContour(outerShape);
  
  beginContour();
  drawSmoothContour(innerHole);
  endContour();
  
  endShape(CLOSE);
}

// Drop-in replacement for curveVertex loops
void drawSmoothContour(PVector[] pts) {
  int n = pts.length;
  if (n < 3) return;

  vertex(pts[0].x, pts[0].y);

  for (int i = 0; i < n; i++) {
    int i0 = (i - 1 + n) % n;     // Previous point
    int i1 = i;                   // Start of this segment
    int i2 = (i + 1) % n;         // End of this segment
    int i3 = (i + 2) % n;         // Next point

    PVector p0 = pts[i0];
    PVector p1 = pts[i1];
    PVector p2 = pts[i2];
    PVector p3 = pts[i3];

    // Convert to Bezier by dividing by 6.0
    float cp1x = p1.x + (p2.x - p0.x) / 6.0;
    float cp1y = p1.y + (p2.y - p0.y) / 6.0;

    float cp2x = p2.x - (p3.x - p1.x) / 6.0;
    float cp2y = p2.y - (p3.y - p1.y) / 6.0;

    bezierVertex(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
  }
}

As of now, this workaround is working really well in the sketch that I’m working on. I’ll update if I run into any issues.

In general, a donut is a grid of circles on a circle.

You can use PShape or so also