How to create a clipping mask that preserves transparency?

I’ve been trying to implement clipping masks for a project and have come up against a roadblock. I want to be able to mask over a PGraphics object that has some transparent pixels while retaining that transparency. The clip() function only works for rectangle clipping areas, which is not sufficient for me, and the mask() function implements an alpha mask, not a clipping mask, so it doesn’t preserve transparency. The result of using mask() is that areas that I want to keep transparent become black, like in this image, where it “should” just show a red circle against a yellow background:

with_mask

As I understand from some experimenting, mask() takes the blue color data in the masking image, and sets the alpha value of the image being masked to that value, regardless of what it was before. What I need is a binary clipping function, that either turns pixels transparent or leaves them untouched. Is there a good, built-in way to do this, or will I need to write my own function? Any advice is appreciated.

1 Like

I found a workable answer, so I’m posting my findings for completeness. It remains to be seen if this solution will scale well with my program, but it seems promising.

I am going with an ‘alpha subtraction’ method. I made that term up, but I think you can do something like this in Photoshop with the right brush and settings. I grab the blue value of the mask and subtract that from the image’s current alpha value. This would produce the opposite effect of a regular alpha mask, so I also subtract the blue value from 255. Subtracting alpha from the image maintains transparency in the way that a regular alpha mask cannot, since it just overrides the value.

Below is demonstrative code. Comment out lines 32 or 33 or both to see the different options.

Here’s what it looks like with the alpha subtraction mask (line 32 commented out):
new_mask

PGraphics pg, mask;

void setup() {
  size(400, 400);
  
  //draw a yellow background
  background(#ffd700);
  
  pg = createGraphics(width, height);
  mask = createGraphics(width, height);
  
  //draw 4 red circles over a transparent background
  pg.beginDraw();
  pg.clear();
  pg.noStroke();
  pg.fill(#D3191C);
  pg.ellipse(width/2,height/2,50,50);
  pg.ellipse(150,150,50,50);
  pg.ellipse(100,100,50,50);
  pg.ellipse(50,50,50,50);
  pg.endDraw();
  
  //create a square clipping mask
  mask.beginDraw();
  mask.background(0);
  mask.noStroke();
  mask.fill(255);
  mask.rect(100,100,200,200);
  mask.endDraw();
  
  //mask the circles
  //pg.mask(mask); // <- std function
  alphaSubtract(pg, mask);  // <- new function
  
  //show the result
  image(pg, 0, 0);
}

void draw() {
}

void alphaSubtract(PGraphics img, PGraphics cm){
  img.loadPixels();
  cm.loadPixels();
  if(img.pixels.length != cm.pixels.length){
    return;
  }
  for(int j = 0; j<img.height; j++){
    for(int i = 0; i<img.width; i++){
      // get argb values
      color argb = img.pixels[(j*img.width) + i];
      int a = argb >> 24 & 0xFF;
      int r = argb >> 16 & 0xFF;
      int g = argb >> 8 & 0xFF;
      int b = argb & 0xFF;
      
      color maskPixel = cm.pixels[(j*img.width) + i];
      int alphaShift = 0xFF - (maskPixel & 0xFF);  //grab blue value from mask pixel
      
      // subtract alphaShift from pixel's alpha value;
      img.pixels[(j*img.width) + i] = color(r,g,b,a-alphaShift);
    }
  }
}

void keyPressed(){
  if(key == 's'){
    save("with_mask.png");
  }
}
3 Likes

Thanks for sharing this solution.

If I’m understanding right, you want to create a mask that is is transparent outside your clipping area AND transparent inside your clipping area if that was the original pixel value. As a related approach, did you look at the alpha data option on the reference page for mask()?

https://processing.org/reference/PImage_mask_.html

In addition to using a mask image, an integer array containing the alpha channel data can be specified directly. This method is useful for creating dynamically generated alpha masks. This array must be of the same length as the target image’s pixels array and should contain only grayscale data of values between 0-255.

Because PImage.mask() takes an int[] array, just extract the minimum alpha channel value from the two source images into an array:

int[] minAlphas(PImage img, PImage img2) {
  img.loadPixels();
  img2.loadPixels();
  int[] a = new int[img.pixels.length];
  for (int i =0; i<img.pixels.length; i++) {
    a[i] = min(img.pixels[i] >> 24 & 0xFF, img2.pixels[i] >> 24 & 0xFF);
  }
  return a;
}

You can then call this in a single line of code:

img.mask(minAlphas(img, mask));

Here is an example:

/**
 * MaskAlphaImage
 * 2019-12 Processing 3.4
 * combine a tranparent image with mask alpha, and use both to mask the image
 * https://discourse.processing.org/t/how-to-create-a-clipping-mask-that-preserves-transparency/16093/2
 */
 
PImage img;
PGraphics mask;

void setup() {
  size(480, 480);
  // load source image
  img = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Processing_3_logo.png/480px-Processing_3_logo.png");
  // create drawable mask image (PGraphics)
  mask = createGraphics(480, 480);
  // and configure draw properties
  mask.beginDraw();
  mask.noStroke();
  mask.rect(60, 60, 260, 260);
  mask.endDraw();
  // apply existing alpha and mask --
  // e.g. both the rectangular mask AND the existing
  // circular cutout around the Processing logo are used
  img.mask(minAlphas(img, mask));
}

void draw() {
  background(255, 0, 0);
  image(img, 0, 0);
}

/**
 * return a minimum alpha channel from two images -- 
 * useful combining a transparent image with a second mask
 * and passing it to mask()
 */
int[] minAlphas(PImage img, PImage img2) {
  img.loadPixels();
  img2.loadPixels();
  int[] a = new int[img.pixels.length];
  for (int i =0; i<img.pixels.length; i++) {
    a[i] = min(img.pixels[i] >> 24 & 0xFF, img2.pixels[i] >> 24 & 0xFF);
  }
  return a;
}

MaskAlphaImage--screenshot

3 Likes

Nice, that’s a really good approach! More condensed too. I can see the two approaches being used for slightly different purposes, as they handle semi-transparent values differently. Like, if a pixel with an alpha value of 128 was masked by a value of 128, they have different results. alphaSubtract() would push the value to 0, while minAlphas() would keep it at 128. Either works fine for what I’m doing now.

1 Like