Mattarn
Mattarn

Reputation: 41

How to create overlapping - not stacking - shapes with canvas?

I'm trying to create an array of shapes that overlap. But I'm having difficulty preventing those shapes stacking on top of one-another.

I guess I want them to mesh together, if that makes sense?

Here's the code:

var overlap_canvas = document.getElementById("overlap");
var overlap_context = overlap_canvas.getContext("2d");
var x = 200;
var y = x;
var rectQTY = 4 // Number of rectangles

overlap_context.translate(x,y);

for (j=0;j<rectQTY;j++){    // Repeat for the number of rectangles

// Draw a rectangle
overlap_context.beginPath();
overlap_context.rect(-90, -100, 180, 80);
overlap_context.fillStyle = 'yellow';
overlap_context.fill();
overlap_context.lineWidth = 7;
overlap_context.strokeStyle = 'blue';
overlap_context.stroke();

// Degrees to rotate for next position
overlap_context.rotate((Math.PI/180)*360/rectQTY);
}

And here's my jsFiddle: http://jsfiddle.net/Q8yjP/

And here's what I'm trying to achieve:

http://postimg.org/image/iio47kny3/

Any help or guidance would be greatly appreciated!

Upvotes: 4

Views: 1791

Answers (4)

KaliedaRik
KaliedaRik

Reputation: 378

Using pure JavaScript ...

<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <canvas id="mycanvas" width="400px" height="400px"></canvas>
        <script>
            window.onload = function(){
                var canvas = document.getElementById('mycanvas');
                var ctx = canvas.getContext('2d');
                //cheat - use a hidden canvas
                var hidden = document.createElement('canvas');
                hidden.width = 400;
                hidden.height = 400;
                var hiddenCtx = hidden.getContext('2d');
                hiddenCtx.strokeStyle = 'blue';
                hiddenCtx.fillStyle = 'yellow';
                hiddenCtx.lineWidth = 5;
                //translate origin to centre of hidden canvas, and draw 3/4 of the image
                hiddenCtx.translate(200,200);
                for(var i=0; i<3; i++){
                    hiddenCtx.fillRect(-170, -150, 300, 120);
                    hiddenCtx.strokeRect(-170, -150, 300, 120);
                    hiddenCtx.rotate(90*(Math.PI/180));
                    }
                //reset the hidden canvas to original status
                hiddenCtx.rotate(90*(Math.PI/180));
                hiddenCtx.translate(-200,-200);
                //translate to middle of visible canvas
                ctx.translate(200, 200);
                //repeat trick, this time copying from hidden to visible canvas
                ctx.drawImage(hidden, 200, 0, 200, 400, 0, -200, 200, 400);
                ctx.rotate(180*(Math.PI/180));
                ctx.drawImage(hidden, 200, 0, 200, 400, 0, -200, 200, 400);
                };
        </script>
    </body>
</html>

Demo on jsFiddle

Upvotes: 0

user1693593
user1693593

Reputation:

You cannot specify this behavior but you can implement an algorithmic-ish approach that uses composite modes.

As shown in this demo the result will be like this:

Squares with overlap

Define line width and the rectangles you want to draw (you can fill this array with the loop you already got to calculate the positions/angles - for simplicity I just use hard-coded ones here):

var lw = 4,
    rects = [
        [20, 15, 200, 75],
        [150, 20, 75, 200],
        [20, 150, 200, 75],
        [15, 20, 75, 200]
    ], ...

I'll explain the line width below.

/// set line-width to half the size
ctx.lineWidth = lw * 0.5;

In the loop you add one criteria for the first draw which is also where you change composite mode. We also clear the canvas with the last rectangle:

/// loop through the array with rectangles
for(;r = rects[i]; i++) {
    ctx.beginPath();
    ctx.rect(r[0], r[1], r[2], r[3]);
    ctx.fill();
    ctx.stroke();

    /// if first we do a clear with last rectangle and
    /// then change composite mode and line width
    if (i === 0) {
        r = rects[rects.length - 1];
        ctx.clearRect(r[0] - lw * 0.5, r[1] - lw * 0.5, r[2] + lw, r[3] + lw);
        ctx.lineWidth = lw;
        ctx.globalCompositeOperation = 'destination-over';
    }
}

This will draw the rectangles and you have the flexibility to change the sizes without needing to recalculate clipping.

The line-width is set separately as stroke strokes the line from the middle. Therefor, since we later use destination-over mode it means half of the line won't be visible as we first fill which becomes part of destination so that the stroke will only be able to fill outside the stroked area (you could reverse the order of stroke and fill but will always run into an adjustment for the first rectangle).

We also need it to calculate the clipping which must include (half) the line on the outside.

This is also why we initially set it to half as the whole line will be drawn the first time - otherwise the first rectangle will have double as thick borders.

Upvotes: 6

Zach Saucier
Zach Saucier

Reputation: 25954

Sadly, the feature you want of setting z-indexes on part of an element using canvas is not available currently. If you just need it for the four rectangle object you could do something like this which hides part of the rectangle to fake the effect you want, however this is hard coded to only 4 rectangles.

var overlap_canvas = document.getElementById("overlap");
var overlap_context = overlap_canvas.getContext("2d");
var x = 200;
var y = x;
var rectQTY = 4 // Number of rectangles

overlap_context.translate(x, y);

for (j = 0; j < rectQTY; j++) { // Repeat for the number of rectangles
    // Draw a rectangle
    overlap_context.beginPath();
    overlap_context.rect(-90, -100, 180, 80);
    overlap_context.fillStyle = 'yellow';
    overlap_context.fill();
    overlap_context.lineWidth = 7;
    overlap_context.strokeStyle = 'blue';
    overlap_context.stroke();
    if (j === 3) {
        overlap_context.beginPath();
        overlap_context.rect(24, -86, 72, 80);
        overlap_context.fillStyle = 'yellow';
        overlap_context.fill();
        overlap_context.closePath();

        overlap_context.beginPath();
        overlap_context.moveTo(20, -89.5);
        overlap_context.lineTo(100, -89.5);
        overlap_context.stroke();
        overlap_context.closePath();

        overlap_context.beginPath();
        overlap_context.moveTo(20.5, -93.1);
        overlap_context.lineTo(20.5, 23);
        overlap_context.stroke();
        overlap_context.closePath();
    }

    // Degrees to rotate for next position
    overlap_context.rotate((Math.PI / 180) * 360 / rectQTY);
}

Demo here

If you have to make it dynamic, you could cut the shapes like Dark Duck suggested or you could try to create a function that detects when an object is overlapped and redraw it one time per rectangle (hard to do and not sure if it'd work). Perhaps you could come up with some equation for positioning the elements in relation to how I have them hard coded now to always work depending on the rotation angle, this would be your best bet IMO, but I don't know how to make that happen exactly

Overall you can't really do what you're looking for at this point in time

Upvotes: 1

DARK_DUCK
DARK_DUCK

Reputation: 1777

The only way to do it to cut your rectangles and compute which sub rectangle goes over which one. But I think you will have to draw your borders and inner rectangles separately because separating rectangles will add additional borders.

Hope it helped

Upvotes: 1

Related Questions