Is there a way to perfectly calculate the difference in radius between an outer rectangle and an inner rectangle?

Viewed 37

Okay, so I have this function:

function roundedRect(x,y,w,h,r1,r2,r3,r4){
    ctx.beginPath();
    ctx.moveTo(x + r1,y);
    ctx.lineTo(x + w - r2,y);
    ctx.arcTo(x + w,y,x + w,y + r2,r2);
    ctx.lineTo(x + w,y + h - r3);
    ctx.arcTo(x + w,y + h,x + w - r3,y + h,r3);
    ctx.lineTo(x + r4,y + h);
    ctx.arcTo(x,y + h,x,y + h - r4,r4);
    ctx.lineTo(x,y + r1);
    ctx.arcTo(x,y,x + r1,y,r1);
    ctx.closePath();
}

This draws a rectangle with arced edges according to values for radii. Here's how it looks in action:

<canvas id = "c" width = "100" height = "100"></canvas>

<script>
    let ctx = document.getElementById("c").getContext("2d");
    function roundedRect(ctx,x,y,w,h,r1,r2,r3,r4){
        ctx.beginPath();
        ctx.moveTo(x + r1,y);
        ctx.lineTo(x + w - r2,y);
        ctx.arcTo(x + w,y,x + w,y + r2,r2);
        ctx.lineTo(x + w,y + h - r3);
        ctx.arcTo(x + w,y + h,x + w - r3,y + h,r3);
        ctx.lineTo(x + r4,y + h);
        ctx.arcTo(x,y + h,x,y + h - r4,r4);
        ctx.lineTo(x,y + r1);
        ctx.arcTo(x,y,x + r1,y,r1);
        ctx.closePath();
    }
    
    ctx.lineWidth = 10;
    ctx.fillStyle = "red";
    ctx.strokeStyle = "black";
    roundedRect(ctx,5,5,90,90,10,10,10,30);
    ctx.stroke();
    ctx.fill();
</script>

Now, let's say I wanted to render one of these rectangles inside the other. The first render is only the stroke, and the second render is only the fill. This looks like so:

<canvas id = "c" width = "100" height = "100"></canvas>

<script>
    let ctx = document.getElementById("c").getContext("2d");
    function roundedRect(ctx,x,y,w,h,r1,r2,r3,r4){
        ctx.beginPath();
        ctx.moveTo(x + r1,y);
        ctx.lineTo(x + w - r2,y);
        ctx.arcTo(x + w,y,x + w,y + r2,r2);
        ctx.lineTo(x + w,y + h - r3);
        ctx.arcTo(x + w,y + h,x + w - r3,y + h,r3);
        ctx.lineTo(x + r4,y + h);
        ctx.arcTo(x,y + h,x,y + h - r4,r4);
        ctx.lineTo(x,y + r1);
        ctx.arcTo(x,y,x + r1,y,r1);
        ctx.closePath();
    }
    
    ctx.fillStyle = "red";
    ctx.strokeStyle = "black";
    ctx.lineWidth = 10;
    roundedRect(ctx,5,5,90,90,10,10,10,30);
    ctx.stroke();
    ctx.beginPath();
    roundedRect(ctx,5,5,90,90,10,10,10,30);
    ctx.fill();
</script>

Everything's looking great so far. But it's not good so far. If I reduce the transparency of the render then you can see a very ugly issue:

<canvas id = "c" width = "100" height = "100"></canvas>

<script>
    let ctx = document.getElementById("c").getContext("2d");
    function roundedRect(ctx,x,y,w,h,r1,r2,r3,r4){
        ctx.beginPath();
        ctx.moveTo(x + r1,y);
        ctx.lineTo(x + w - r2,y);
        ctx.arcTo(x + w,y,x + w,y + r2,r2);
        ctx.lineTo(x + w,y + h - r3);
        ctx.arcTo(x + w,y + h,x + w - r3,y + h,r3);
        ctx.lineTo(x + r4,y + h);
        ctx.arcTo(x,y + h,x,y + h - r4,r4);
        ctx.lineTo(x,y + r1);
        ctx.arcTo(x,y,x + r1,y,r1);
        ctx.closePath();
    }
    
    ctx.globalAlpha = 0.3;
    ctx.fillStyle = "red";
    ctx.strokeStyle = "black";
    ctx.lineWidth = 10;
    roundedRect(ctx,5,5,90,90,10,10,10,30);
    ctx.stroke();
    ctx.beginPath();
    roundedRect(ctx,5,5,90,90,10,10,10,30);
    ctx.fill();
</script>

You can see that the edges of the fill overlap the stroke, and it causes a very ugly effect. The obvious solution is to scoot the fill in a bit (increase its x and y by half the stroke width, decrease its width and height by full stroke width) but that causes there to be an ugly, unwanted gap in each corner as can be seen below:

<canvas id = "c" width = "100" height = "100"></canvas>

<script>
    let ctx = document.getElementById("c").getContext("2d");
    function roundedRect(ctx,x,y,w,h,r1,r2,r3,r4){
        ctx.beginPath();
        ctx.moveTo(x + r1,y);
        ctx.lineTo(x + w - r2,y);
        ctx.arcTo(x + w,y,x + w,y + r2,r2);
        ctx.lineTo(x + w,y + h - r3);
        ctx.arcTo(x + w,y + h,x + w - r3,y + h,r3);
        ctx.lineTo(x + r4,y + h);
        ctx.arcTo(x,y + h,x,y + h - r4,r4);
        ctx.lineTo(x,y + r1);
        ctx.arcTo(x,y,x + r1,y,r1);
        ctx.closePath();
    }
    
    ctx.globalAlpha = 0.3;
    ctx.fillStyle = "red";
    ctx.strokeStyle = "black";
    ctx.lineWidth = 10;
    roundedRect(ctx,5,5,90,90,10,10,10,30);
    ctx.stroke();
    ctx.beginPath();
    roundedRect(ctx,10,10,80,80,10,10,10,30);
    ctx.fill();
</script>

A fix for this would be to simply calculate a new radius for each corner. This absolutely works so long as it's done right. I found that multiplying by 0.666 seems to yield a very close result most of the time, and it's what I've been using up until now. But, as can be seen below, it's far from perfect and its imperfections only grow stronger with larger radii.

<canvas id = "c" width = "100" height = "100"></canvas>

<script>
    let ctx = document.getElementById("c").getContext("2d");
    function roundedRect(ctx,x,y,w,h,r1,r2,r3,r4){
        ctx.beginPath();
        ctx.moveTo(x + r1,y);
        ctx.lineTo(x + w - r2,y);
        ctx.arcTo(x + w,y,x + w,y + r2,r2);
        ctx.lineTo(x + w,y + h - r3);
        ctx.arcTo(x + w,y + h,x + w - r3,y + h,r3);
        ctx.lineTo(x + r4,y + h);
        ctx.arcTo(x,y + h,x,y + h - r4,r4);
        ctx.lineTo(x,y + r1);
        ctx.arcTo(x,y,x + r1,y,r1);
        ctx.closePath();
    }
    
    ctx.globalAlpha = 0.3;
    ctx.fillStyle = "red";
    ctx.strokeStyle = "black";
    ctx.lineWidth = 10;
    roundedRect(ctx,5,5,90,90,10,10,10,30);
    ctx.stroke();
    ctx.beginPath();
    roundedRect(ctx,10,10,80,80,10 * 0.666,10  * 0.666,10 * 0.666,30 * 0.666);
    ctx.fill();
</script>

So now finally onto my question... Is there any means of perfectly calculating this new radius? Is there some magic number to multiply by which will always yield 100% perfect results? Is it a lot harder than I expect? In my final result, I want a way to make these two rectangles line up perfectly no matter the radius.

1 Answers

Alright, so I spent about an hour or so just aimlessly messing around with these values, and I accidentally came across a perfect solution. The value for the border radius of the inner rectangle is: outerRectangleRadius - outerRectangleStrokeWidth / 2

So far this has worked perfectly for anything that I can throw at it and I'm very pleased! :) I hope that this question + answer really helps a lot of people in the future.

<canvas id = "c" width = "100" height = "100"></canvas>

<script>
    let ctx = document.getElementById("c").getContext("2d");
    function roundedRect(ctx,x,y,w,h,r1,r2,r3,r4){
        ctx.beginPath();
        ctx.moveTo(x + r1,y);
        ctx.lineTo(x + w - r2,y);
        ctx.arcTo(x + w,y,x + w,y + r2,r2);
        ctx.lineTo(x + w,y + h - r3);
        ctx.arcTo(x + w,y + h,x + w - r3,y + h,r3);
        ctx.lineTo(x + r4,y + h);
        ctx.arcTo(x,y + h,x,y + h - r4,r4);
        ctx.lineTo(x,y + r1);
        ctx.arcTo(x,y,x + r1,y,r1);
        ctx.closePath();
    }

    
    ctx.globalAlpha = 0.3;
    ctx.fillStyle = "red";
    ctx.strokeStyle = "black";

    const strokeWidth = 10;
    ctx.lineWidth = strokeWidth;
    
    const radii = [10,40,20,30];

    // draw outer square
    roundedRect(ctx,5,5,90,90,radii[0],radii[1],radii[2],radii[3]);
    ctx.stroke();
    // draw inner square
    ctx.beginPath();
    
    roundedRect(ctx,5 + strokeWidth / 2,5  + strokeWidth / 2,90 - strokeWidth,90 - strokeWidth,radii[0] - strokeWidth / 2,radii[1] - strokeWidth / 2,radii[2] - strokeWidth / 2,radii[3] - strokeWidth / 2);
    ctx.fill();
</script>

Related