javascripthtmlcanvashtml5-canvasglobalcompositeoperation

JavaScript canvas clipping shape when out of bounds


What I'm asking for may be extremely easy, but I've been having quite a bit of trouble getting the intended result.

I want a shape (in this example it's squares but should work with other shapes such as circles, etc) to cut itself off when it leaves the bounds of another shape.

Basically, top image is what I want, bottom is what I currently have: https://i.sstatic.net/vxSLj.jpg

I heard this can be done with globalCompositeOperation, but am looking for any solution that will give the wanted result.

This is the current code, if you can't use JSFiddle:

// Fill the background
ctx.fillStyle = '#0A2E36';
ctx.fillRect(0, 0, canvas.width, canvas.height);

// Fill the parent rect
ctx.fillStyle = '#CCA43B';
ctx.fillRect(100, 100, 150, 150);

// Fill the child rect
ctx.fillStyle = 'red';
ctx.fillRect(200, 200, 70, 70);

// And fill a rect that should not be affected
ctx.fillStyle = 'green';
ctx.fillRect(80, 80, 50, 50);

JSFiddle Link


Solution

  • Since you need some kind of relation between objects - a scene graph -, you should build it now.
    From your question, it seems that any child element should be drawn clipped by its parent element.
    (Yes composite operation could come to the rescue, but they are handy only when drawing like 2 figures on a cleared background, things get quickly complicated otherwise, and you might have to use a back canvas, so clipping is simpler here.)

    I did below a most basic class that handles the rect case, you'll see that it isn't very difficult to build.

    The 'scene' is made out of a background Rect, which has two childs, the yellow and the green. And the yellow Rect has a red child.

    var canvas = document.getElementById('cv');
    var ctx = canvas.getContext('2d');
    
    function Rect(fill, x, y, w, h) {
        var childs = [];
        this.draw = function () {
            ctx.save();
            ctx.beginPath();
            ctx.fillStyle = fill;
            ctx.rect(x, y, w, h);
            ctx.fill();
            ctx.clip();
            for (var i = 0; i < childs.length; i++) {
                childs[i].draw();
            }
            ctx.restore();
        }
        this.addChild = function (child) {
            childs.push(child);
        }
        this.setPos = function (nx, ny) {
            x = nx;
            y = ny;
        }
    }
    
    // background
    var bgRect = new Rect('#0A2E36', 0, 0, canvas.width, canvas.height);
    // One parent rect
    var parentRect = new Rect('#CCA43B', 100, 100, 150, 150);
    // child rect
    var childRect = new Rect('red', 200, 200, 70, 70);
    parentRect.addChild(childRect);
    //  Another top level rect
    var otherRect = new Rect('green', 80, 80, 50, 50);
    
    bgRect.addChild(parentRect);
    bgRect.addChild(otherRect);
    
    function drawScene() {
        bgRect.draw();
        drawTitle();
    }
    
    drawScene();
    
    window.addEventListener('mousemove', function (e) {
        var rect = canvas.getBoundingClientRect();
        var x = (e.clientX - rect.left);
        var y = (e.clientY - rect.top);
        childRect.setPos(x, y);
        drawScene();
    });
    
    function drawTitle() {
        ctx.fillStyle = '#DDF';
        ctx.font = '14px Futura';
        ctx.fillText('Move the mouse around.', 20, 24);
    }
    <canvas id='cv' width=440 height=440></canvas>