Cookie cut shape from other shape

edited January 2016 in p5.js Programming Questions

I am trying to cut a rectangular hole into a red circular shape, making an image that I can later stamp -- image(myImage, x, y) -- at different canvas locations.

Something like this:

circle

I assume I need to create 2 graphics objects (createGraphics), fill one with a red ellipse, fill the other with a smaller (black, white?) rectangular shape. What I am missing is how to do the cutting (masking) of one from the other. I've tried several blend modes but they don't seem to give me the results I want.

Thanks in advance. Alex

Answers

  • edited May 2015

    I'm not sure

    but when you mean a hole in the center as in "I can see through the hole the image behind it":

    • there's no such thing as a hole afaik.

    • you might be better off to draw the red as 4 separate parts (arc) and just don't draw the center in the first place. That'd give you a hole.

    fill the other with a smaller (black, white?) rectangular shape

    that wouldn't give you no transparency but just a white rectangle

    ;-)

  • Answer ✓

    There's a 'mask' listed in the p5 reference but i can't get the page to open at the moment :/

    If it's been implemented it should work as per the processing example. Otherwise you could create a vector shape with a hole in and create an image from that.

  • thanks, I didn't find mask in the reference...

  • mask was the ticket. Thanks for this.

    However, it looks like p5.Graphics objects don't have a mask method while p5.Image objects do. So I ended up doing this:

    1. use createImage and manipulate pixels[] to create the circle
    2. then, use createGraphics to create the rectangle
    3. then, call circle.mask(rect)

    and done, almost. On my Retina display, the resulting stamp was too small. Calling devicePixelScaling(false) solved it.

    Thanks again.

  • edited May 2015

    @alexR, I've answered something similar in this forum thread:
    http://forum.processing.org/two/discussion/10291/issue-with-color-alpha

    The issue is that we need to rect() w/ an opaque fill() in order to override the bigger ellipse()'s.
    But you want that rect() to be 100% transparent though.
    My solution is to hunt down every pixel which matches w/ rect()'s fill().
    Then assign its alpha channel to 0 if so. :D

    For now, see the "Java Mode" version 1st. Gonna paste p5.js's 1 next post: :P

    /**
     * SetAllColorAlpha II (v1.02)
     * by GoToLoop (2015/May/30)
     *
     * forum.processing.org/two/discussion/11066/cookie-cut-shape-from-other-shape
     * forum.processing.org/two/discussion/10291/issue-with-color-alpha
     */
    
    static final color CIRCLE = #FF0000, HOLE = 0;
    PImage img;
    
    void setup() {
      size(400, 200, JAVA2D);
      smooth(4);
      noLoop();
    
      imageMode(CORNER);
      background(#00C0FF); // 100% opaque cyan canvas.
    
      img = maskedCircle(width>>1, height, CIRCLE, HOLE);
      image(img, 0, 0); // square is 100% opaque black.
    
      // Now black color becomes 100% transparent:
      image(setAllColorAlpha(img, HOLE, 0), width>>1, 0);
    }
    
    PImage maskedCircle(int w, int h, color circle, color hole) {
      PGraphics pg = createGraphics(w, h, JAVA2D);
      pg.beginDraw();
    
      pg.smooth(4);
      pg.noStroke();
    
      pg.ellipseMode(CORNER);
      pg.rectMode(CENTER);
    
      pg.fill(circle);
      pg.ellipse(0, 0, w, h);
    
      pg.fill(hole);
      pg.rect(w>>1, h>>1, w/3, h/3);
    
      pg.endDraw();
      return pg.get();
    }
    
    static final PImage setAllColorAlpha(PImage img, color c, color a) {
      c  &= ~#000000; // clear alpha channel w/ inverted alpha mask.
      a <<= 030; // shift alpha value to most significant byte (MSB) location.
    
      img.loadPixels();
      color p[] = img.pixels, i = p.length, q;
    
      while (i-- != 0)  if ((q = p[i] & ~#000000) == c)  p[i] = q | a;
    
      img.updatePixels();
      return img;
    }
    
  • edited May 2015

    Here it is the p5.js version. However it's somewhat very buggy! :-&
    noStroke(), ellipseMode() & rectMode() aren't working for createGraphics() at all! ~X(
    We can paste and run it from here if we wish: http://p5js.org/reference/#/p5/clear

    /**
     * SetAllColorAlpha II (v1.02)
     * by GoToLoop (2015/May/30)
     *
     * forum.processing.org/two/discussion/11066/cookie-cut-shape-from-other-shape
     * forum.processing.org/two/discussion/10291/issue-with-color-alpha
     */
    
    const CIRCLE = Object.freeze([0xFF, 0, 0]), HOLE = Object.freeze([0, 0, 0]);
    var img;
    
    function setup() {
      createCanvas(400, 200, P2D);
      smooth().noLoop();
    
      imageMode(CORNER);
      background(0, 0xC0, 0xFF); // 100% opaque cyan canvas.
    
      img = maskedCircle(width>>1, height, CIRCLE, HOLE);
      image(img, 0, 0); // square is 100% opaque black.
    
      // Now black color becomes 100% transparent:
      image(setAllColorAlpha(img, HOLE, 0), width>>1, 0);
    }
    
    function maskedCircle(w, h, circle, hole) {
      const pg = createGraphics(w, h, P2D);
    
      // Not working as expected:  
      pg.smooth(), pg.noStroke();
      pg.ellipseMode(CORNER), pg.rectMode(CENTER);
    
      // Workaround until p5.Graphics class is finally fixed (if ever!):
      smooth().noStroke();
      ellipseMode(CORNER).rectMode(CENTER);
    
      pg.fill(circle), pg.ellipse(0, 0, w, h);
      pg.fill(hole), pg.rect(w>>1, h>>1, w/3, h/3);
    
      return pg.get();
    }
    
    function setAllColorAlpha(img, ink, alpha) {
      img.loadPixels();
      const pix = img.pixels;
    
      outer: for (var i = 0, j = 0; i != pix.length; i += 4, j = 0) {
        while (j != 3)  if (pix[i+j] != ink[j++])  continue outer;
        pix[i+3] = alpha;
      }
    
      img.updatePixels();
      return img;
    }
    
  • edited May 2015

    As a way to circumvent the buggy p5.Graphics class, we can change canvas' styles instead. /:)

    So instead of the expected code below which would address the p5.Graphics individually:

    pg.smooth(), pg.noStroke();
    pg.ellipseMode(CORNER), pg.rectMode(CENTER);
    

    We go w/ directly changing p5's global canvas itself: :O)

    smooth().noStroke();
    ellipseMode(CORNER).rectMode(CENTER);
    
  • Thanks a lot for your in-depth answer, GoToLoop. It's too bad that p5.graphics is buggy and that it doesn't implement masking. This would have made p5 a solid contender for my current project, other features I have yet to look into notwithstanding.

  • edited January 2016

    ... is buggy and that it doesn't implement masking.

    My solution doesn't demand any mask() b/c the inner square was made fully transparent already.
    But I dunno where did you get the idea that p5.js doesn't support mask()! :-\"

    Indeed, as @blindfish has discovered, its true mask()'s web reference is buggy as well: :O)
    http://p5js.org/reference/#/p5/mask

    However, I've found out some working example here: :-$
    http://p5js.org/examples/examples/Image_Alpha_Mask.php

  • edited June 2015

    Oh, now I've got what you meant: That only p5.Graphics lacks mask(). b-(
    Much probably b/c p5.Graphics doesn't necessarily "inherits" from p5.Image like in "Java Mode". ~:>

    But fix is very simple... Just do what I did at the end of maskedCircle(): pg.get(); >-)
    Method get() w/o or w/ 4 arguments returns a new p5.Image after all:
    http://p5js.org/reference/#/p5/get

  • edited June 2015

    Forum editor problem. See next post.

  • edited June 2015

    p5.Graphics.get... I had missed that and it sure helps. Thanks a lot for that.

    I added a punchOut (opposite of mask) method to p5.Image:

        // Extend p5.Image, adding the converse of "mask", naming it "punchOut":
        p5.Image.prototype.punchOut = function(p5Image) {
    
            if(p5Image === undefined){
                p5Image = this;
            }
            var currBlend = this.drawingContext.globalCompositeOperation;
    
            var scaleFactor = 1;
            if (p5Image instanceof p5.Graphics) {
                scaleFactor = p5Image._pInst._pixelDensity;
            }
    
            var copyArgs = [
                p5Image,
                0,
                0,
                scaleFactor*p5Image.width,
                scaleFactor*p5Image.height,
                0,
                0,
                this.width,
                this.height
            ];
    
            this.drawingContext.globalCompositeOperation = "destination-out";
            this.copy.apply(this, copyArgs);
            this.drawingContext.globalCompositeOperation = currBlend;
        };
    

    Then tested it: success.

    function setup() {
        // Preps
        devicePixelScaling(false);
        createCanvas(300,100);
        background('cyan');
    
        // The shape
        var disc = createGraphics(100,100);
        disc.noStroke();
        disc.fill('#F00');
        disc.ellipse(50,50,100,100);
    
        // The image of the shape, ready for punching
        var img = disc.get();
    
        // The punch
        var punch = createGraphics(100,100);
        punch.noStroke();
        punch.fill(255);
        punch.rect(25,25,50,50);
    
        // Punch it!
        img.punchOut(punch);
    
        // Tada!
        image(disc,0,0);
        image(punch,100,0);
        image(img, 200, 0);
    }
    

    Thanks again.

Sign In or Register to comment.