Cut-out shapes and image masking in processing(JS)

In processing, it is not really easy to construct complex 2D geometry by subtracting shapes from each other, i.e., creating cut-outs. You can resort to vertex contours, but if you just want to punch holes into a square, what can you do? Especially, if you want to run in the browser using processing.js, it gets somewhat tricky.

A pixel-based approach is to render the complex shape into an off-screen image, and draw the result transparently onto the display. The example discussed below is about creating a rather simple planet with a ring:

See the full listing at the end for the complete code.

blendMode()

In processing 2, the new blendMode() function can be used to overwrite part of a shape with alpha 0 pixels:

PGraphics planetImage;

void setup()
{
   // blendMode() doesn't work properly with the default renderer, use P2D here
  size(500, 500, P2D);

  // create off-screen buffer with transparent background
  planetImage = createGraphics(200, 200, P2D);
  planetImage.beginDraw();
  planetImage.background(0, 0);
  
  planetImage.translate(100, 100);
  planetImage.rotate(0.5);
  
  // draw circle
  planetImage.noStroke();
  planetImage.fill(255, 220, 0);
  planetImage.ellipse(0, 0, 200, 100);

  // replace part of the circle with alpha 0 - "make a hole"
  planetImage.blendMode(REPLACE);
  planetImage.fill(255, 255,  255, 0);
  planetImage.noStroke();
  planetImage.ellipse(0, -5, 160, 70);

  // add "planet"
  planetImage.fill(255, 220, 0, 255);
  planetImage.ellipse(0, 0, 120, 120);

  planetImage.endDraw();
} 

The result can be rendered nicely on top of a background with image(planetImage, …). Unfortunately, blendMode() doesn’t work yet with processing.js, and the blend() function doesn’t allow you to overwrite the alpha channel in the same way as blendMode(REPLACE).

Image masking

Another approach to the problem is to render the shape onto a black background, and then create a mask image to cut out the shape transparently. In processing 2, this can be done using the PImage.mask() method. But again, mask() is not yet supported in processing.js. Instead, we can create a separate mask image, and render our shape in two passes using blend():

PImage maskImage;

void setup()
{
  ...
  
  // create a white on black mask image by thresholding the off-screen image
  maskImage = planetImage.get();
  maskImage.filter(THRESHOLD, 0.1);
}

void draw()
{
  ...
  
  // subtract white planet image - results in a black planet
  blend(maskImage, 0, 0, 200, 200, xPos, yPos, 200, 200, SUBTRACT);
  
  // add colored planet image on top
  blend(planetImage, 0, 0, 200, 200, xPos, yPos, 200, 200, ADD);
} 

If you wanted to use the color black in the solid part of the shape, creation of the mask would have to be modified, obviously. Another disadvantage of this approach is that you cannot transform the images rendered with blend(). I.e., it is usually easy to draw a rotated image using rotate(angle) followed by image(…). With the blend() function, the rotation just doesn’t work.

Phew! So, as promised, here is the complete working example:

PGraphics planetImage;
PImage bgImage;
PImage maskImage;

void setup()
{
size(500, 500);

// create off-screen planet shape with black background (rendered transparently later on)

// create off-screen buffer with black background
planetImage = createGraphics(200, 200);
planetImage.beginDraw();
planetImage.background(0);

planetImage.translate(100, 100);
planetImage.rotate(0.5);

// draw circle
planetImage.noStroke();
planetImage.fill(255, 220, 0);
planetImage.ellipse(0, 0, 200, 100);

// cut out part of the circle
planetImage.fill(0);
planetImage.noStroke();
planetImage.ellipse(0, -5, 160, 70);

// add "planet"
planetImage.fill(255, 220, 0);
planetImage.ellipse(0, 0, 120, 120);

planetImage.endDraw();

// create a white on black mask image by thresholding the off-screen image
maskImage = planetImage.get();
maskImage.filter(THRESHOLD, 0.1);

// draw a background pattern...
background(0, 0, 120);
fill(255);
stroke(255, 255, 255, 50);
strokeWeight(2);
for (int i = 0; i < 100; ++i)
{
float size = random(2, 5);
ellipse(random(width), random(height), size, size);
}

// ... and save it to an image, so we can re-render it easily
bgImage = get();
}

void draw()
{
// draw background pattern
image(bgImage, 0, 0);

int xPos = frameCount % (width + 200) - 200;
int yPos = 100;

// subtract white planet image - results in a black planet
blend(maskImage, 0, 0, 200, 200, xPos, yPos, 200, 200, SUBTRACT);

// add colored planet image on top
blend(planetImage, 0, 0, 200, 200, xPos, yPos, 200, 200, ADD);
}