How to penetrate or cut holes through a 2D foreground

I'm currently making a 2D game in Javascript, but I want to the game to have different lighting levels, for example, if I were to create a day and night cycle. However, I want to be able to cut holes in the lighting/foreground, or do something so that I can make certain parts of the screen lit up, for example like a flashlight or candle. Note: I'm also using the P5.js library.

The most obvious idea that came to mind for going about in creating a foreground is just creating a rectangle with some opacity that covers the entire screen. This is good, but how am I supposed cut through this? Obviously, the code below won't work because I'm just layering on another element, and the rectangle is still obstructed and not perfectly clear.

function setup() {
  noStroke();
  createCanvas(400, 400);
}

function draw() {
  background(255); //white
  
  fill(255, 0, 0);
  rect(200, 200, 25, 25); //Example object
  
  fill(150, 150, 150, 100); //Gray with opacity
  rect(0, 0, 400, 400); //Darkness covering entire screen, foreground
  
  fill(255, 255, 255, 100)
  ellipse(mouseX, mouseY, 50, 50); //object that is supposed to penetrate foreground.
}
<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8" />

  </head>
  <body>
    <script src="sketch.js"></script>
  </body>
</html>

How would I go about doing this? I want this to work not only with just a plain background and simple shapes, but with images as well. Is there a way to cut holes in shapes, or do I need to use something else, like a shader or mask?

Thanks.


Solution 1:

The erase() function may be what you are looking for. It is more flexible than trying to explicitly paint over the areas you want to cover (such as in the approach of using the stroke of a circle and rectangle to cover everything except a circle). And it is easier to use than beginContour() since you can use it with any of the built in drawing primitives (rect, ellipse, triangle, etc).

let overlay;

function setup() {
  noStroke();
  createCanvas(windowWidth, windowHeight);
  overlay = createGraphics(width, height);
  overlay.noStroke();
  overlay.fill(255);
  overlay.background(150, 150, 150, 200);
}

function mouseMoved() {
  // Only update the overlay when something changes
  // Clear the overlay so that alpha doesn't accumulate
  overlay.clear()
  //Gray with opacity, covering entire screen, foreground
  overlay.background(150, 150, 150, 200);

  // The color and alpha values for shapes drawn when erasing basically do not matter. You can effect the % of erasure with the arguments erase.
  overlay.erase(100);
  overlay.ellipse(mouseX, mouseY, 50, 50); //object that is supposed to penetrate foreground.
  overlay.noErase();
}

function draw() {
  background(255); //white

  fill(255, 0, 0);
  rect(width / 2, height / 2, 25, 25); //Example object

  image(overlay, 0, 0, width, height);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

If you want to remove shapes from your overlay with more complete alpha channel support you can use blendMode(ERASE).

let overlay;
let overlayCtx;

let gradient;

function setup() {
  noStroke();
  createCanvas(windowWidth, windowHeight);
  overlay = createGraphics(width, height);
  overlayCtx = overlay.drawingContext;
  overlay.noStroke();
  overlay.background(150, 150, 150, 200);

  // Using the raw canvas API to make a radial gradient
  gradient = overlayCtx.createRadialGradient(0, 0, 5, 0, 0, 25);
  // The colors here don't matter, only the alpha channel
  gradient.addColorStop(0, 'rgba(0,0,0,1)');
  gradient.addColorStop(1, 'rgba(0,0,0,0)');
  overlayCtx.fillStyle = gradient;
}

function mouseMoved() {
  // Only update the overlay when something changes
  // Clear the overlay so that alpha doesn't accumulate
  overlay.clear()
  //Gray with opacity, covering entire screen, foreground
  overlay.background(150, 150, 150, 200);

  overlay.push();
  // Use blend mode REMOVE to remove the color (using only the alpha channel?)
  overlay.blendMode(REMOVE);
  // Because of the way gradients work we have to translate and draw our ellipse at the origin.
  overlay.translate(mouseX, mouseY);
  overlay.ellipse(0, 0, 50, 50); //object that is supposed to penetrate foreground.
  overlay.blendMode(BLEND);
  overlay.pop();
}

function draw() {
  background(255); //white

  fill(255, 0, 0);
  rect(width / 2, height / 2, 25, 25); //Example object

  image(overlay, 0, 0, width, height);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>