Skip to content
Advertisement

Efficiently mask shapes using createGraphics in p5.js

I am trying to create various shapes in p5.js and fill them with specific patterns/drawings. Each shape will have a unique pattern generated using createGraphics. Since my different shapes won’t cover all of my base canvas, I am thinking of creating smaller-sized graphics for my patterns to improve performance. For example, if my base canvas is 1000 * 1000 pixels and my shape only takes 50 * 50 pixels and I want to fill it with a rectangular pattern, I do not see the point in creating a 1000 * 1000 pixels pattern graphics and masking it with my 50 * 50 pixels shape.

I am trying to work on a proof of concept using a single shape and pattern:

  1. Determine the width and height of a rectangle that would contain my entire shape. Note that I know what my shape will look like before drawing it (pre-determined points to draw vertices).
  2. Create my pattern graphics using the dimensions determined in 1.
  3. Create my shape.
  4. Mask the pattern graphics created in 2 with the shape created in 3.
  5. Display the resulting image in my base canvas.

Please also note that a shape can be located at any position within my base canvas and that its position is determined by the first point used to draw the shape. The process will have to be repeated many times to generate the desired output.

function setup() {
  
    createCanvas(1000, 1000);
    background(200, 255, 255);
    
    var ptrn;
    var myShape;
    var msk;

    let ptrn_w = 1000;
    let ptrn_h = 1000;
    
    ptrn = createGraphics(ptrn_w, ptrn_h);
    ptrn.background(120, 0, 150);
    ptrn.fill(255, 255, 255);
    ptrn.noStroke();

    for (let i = 0; i < 500; i++){
        let x = random(0, ptrn_w);
        let y = random(0, ptrn_h);
        ptrn.rect(x, y, 20, 5);
    }

    msk = createGraphics(ptrn_w, ptrn_h);
    msk.beginShape();
    msk.vertex(250, 920);
    msk.vertex(300, 15);
    msk.vertex(325, 75);
    msk.vertex(350, 840);
    msk.endShape(CLOSE);
    
    ( myShape = ptrn.get() ).mask( msk.get() );
    
    image(myShape, 0, 0);
    
}

function draw() {  
    
}

The code listed above works since ptrn_w and ptrn_h are equal to the width and height of the base canvas. However, we are generating patterns/graphics for the whole canvas and a high % of that area is not used. If we are generating hundreds of different shapes with complex patterns, I can see how limiting the area in which we generate our patterns could be beneficial from a performance standpoint.

Changing ptrn_w and ptrn_h to ptrn_w = 100 and ptrn_h = 905 is problematic since the mask would be applied outside of the pattern graphics ptrn.

Is there a way to translate the position of ptrn so that it aligns with the position of msk? Would image(myShape, 0, 0) be problematic if we are zeroing the position of our image?

Another idea I had was to zero the position of my mask and reposition it when calling image(myShape, x_pos, y_pos).

What is the best approach to achieve such behavior? Any creative ideas are welcomed.

Advertisement

Answer

I believe the most efficient way to do this would be to use the underlying CanvasRenderingContext2D’s globalCompositeOperation. By setting this to 'source-in' you can do a sort of reverse mask operation without the need to use a p5.Image object. Additional as shown in the example bellow you can do some simple calculations on your list of vertices to determine the width and height and to draw your shape on a minimally sized buffer.

let vertices = [
  [10, 15],
  [150, 40],
  [100, 120],
  [25, 75]
];

let ptrn;
let myShape;

let shapeTop, shapeLeft;

function setup() {
  createCanvas(windowWidth, windowHeight);

  // Calculate the rage of vertices values
  let shapeBottom, shapeRight;
  for (const [x, y] of vertices) {
    if (shapeTop === undefined || y < shapeTop) {
      shapeTop = y;
    }
    if (shapeBottom === undefined || y > shapeBottom) {
      shapeBottom = y;
    }
    if (shapeLeft === undefined || x < shapeLeft) {
      shapeLeft = x;
    }
    if (shapeRight === undefined || x > shapeRight) {
      shapeRight = x;
    }
  }

  // Determine the minimum width and height to fit the shape
  let w = shapeRight - shapeLeft,
    h = shapeBottom - shapeTop;

  // Generate the pattern
  ptrn = createGraphics(w, h);
  ptrn.background(120, 0, 150);
  ptrn.noStroke();
  for (let i = 0; i < 100; i++) {
    ptrn.fill(random(255), random(255), random(255));
    let x = random(0, w);
    let y = random(0, h);
    ptrn.rect(x, y, 5);
  }

  // Draw the shape 
  myShape = createGraphics(w, h);
  myShape.fill(255);
  myShape.noStroke();
  myShape.beginShape();
  for (const [x, y] of vertices) {
    myShape.vertex(x - shapeLeft, y - shapeTop);
  }
  myShape.endShape(CLOSE);

  // See: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
  // The 'source-in' composite operation will output the drawn content (ptrn)
  // only in place of existing pixels that are not transparent.
  // The output of each pixel is basically: color(r2, g2, b3, min(a1, a2))
  myShape.drawingContext.globalCompositeOperation = 'source-in';
  myShape.image(ptrn, 0, 0)
}

function draw() {
  background(200, 255, 255);
  image(myShape, mouseX + shapeLeft, mouseY + shapeTop);
}
html, body { margin: 0; padding: 0; overflow: hidden }
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement