Creating an Element Mask with a hole in the middle for clicks

Hello! Tried to get this solved on Discord but to no avail.
Messages below:

What’s the right way to make a mask that’s like a “spotlight”, where there’s like black everywhere, but a little circle that you can position that will cut a hole in that black

Totally stumped on this kind of inverse mask

I am starting to think I need to dynamically render an image using canvas and set that to the texture or something…?

var canvas = document.createElement('canvas');

var context = canvas.getContext("2d");
context.arc(50, 50, 30, 0, 2 * Math.PI);
context.fill();

var img = new Image();
img.src = canvas.toDataURL(); document.body.appendChild(img);

context.clearRect(0, 0, canvas.width, canvas.height);

Ah shoot, and I need the portion inside the spotlight to be clickable, but the black portion to not, seems like useInput just takes the bounding rect to eat inputs

Aight so my janky thing for now is to generate 4 rectangles to create a rectangular hole where input works, but this seems super wrong lmao, especially since I am showing the user a clickable ellipse

Steven 06/02/2022

I would use an element or html cutout and on every click, do a simple circle check to see if its in done in the cut out

Bogden 06/02/2022

How would I still pass the click through to other stuff behind it though? The circle check would eat the input event right?

Steven Yesterday at 12:42 AM

Have a custom click event for everything else to listen for

Bogden Yesterday at 8:50 AM

That would mean giving up all of the benefits from using the element input system right? Like bubbling to parents, stopping propagation, blocking input from elements behind other elements, etc right?

Steven Yesterday at 2:10 PM

Hmm , if you need it to work with UI elements, I would create the circle cutout as a playcanvas ui elements instead and do the blocking at that level

You have the top most UI element (that is in front of everything), detect all the standard input events. Check if its out of circle, stop the propergation

Bogden Yesterday at 3:51 PM

Doesn’t that only work if that topmost element is a child over everything else?

Like if entity A is a sibling of B, and in front of it, if I click on A, can I still trigger a click on B?

Steven Yesterday at 3:54 PM

IIRC, it should work as long as its being rendered on top of everything else as it should get the input event first

Bogden Yesterday at 3:55 PM

will that just happen by default? cause AFAIK it eats the input event

Yeah so like in this example project @Steven PlayCanvas 3D HTML5 Game Engine

The green overlay eats the inputs of the things behind it

I was thinking that element masks (for scroll views) allow click throughs, maybe worth diving into the code in the the engine about that?

@jpaulo Any ideas on this?

I think you could try writing a custom UI Shader that does that cut-out for you, and then as Steven mentioned, stop propagation manually if the event is outside the circle. The scroll view system uses regular masks which achieve the opposite effect.

As for the UI shader, it would be implemented and applied via code only (so you wouldn’t be able to preview it on the editor). You can use this example PlayCanvas Examples as a basis for your shader, but instead of inverting the color, simply setting the alpha (.a) to 0 when inside a circle. You’ll need to add additional shader parameters to control the position and radius.

1 Like

Thanks for the suggestion! I have the dynamic rendering of the cut-out working fine, but I’m still not clear on how to do the event passing. In the example project here PlayCanvas 3D HTML5 Game Engine

How would I get events to keep going through to the elements beneath the green one?

Oh right, I see!
Hm we unfortunately do not have any out-of-the-box solution for this, so what I would do is:

  • Disable Use input in the Element that has the mask image
  • In every script that could receive inputs, add a manual check similar to:
    const maskCenterX = window.innerWidth * 0.5;
    const maskCenterY = window.innerHeight * 0.5;
    const maskRadius = 200; // in screen pixels
    const maskRadiusSqr = maskRadius * maskRadius;

    this.entity.element.on("mousedown", (event) => {
        // if outside of the mask, stop event propagation and immediately return
        const distance = Math.pow(event.x - maskCenterX, 2) + Math.pow(event.y - maskCenterY, 2);
        if (distance > maskRadiusSqr) {
            event.stopPropagation();
            return;
        }

        // inside mask circle, go ahead
        console.log(`Hello, I am ${this.entity.name}`);
    });

Ah jeez, that’s a ton of overhead for applying it to everything that could possibly get clicked haha.

Thanks for the solution though! Will probably stick with the method of creating some invisible elements to block clicks on top. Right now it’s just 4 rectangles, but could easily make it 8 and form an octagon without too much trouble. Super janky, but alas.

1 Like