1

I'm trying to get the exact formula in order to compute CSS border-radius property in a canvas. I've already tried and have an example in javascript (see below), but with no success.

Its seems that the browser is still adding some adaptations in order to ajust the borders. And i'm unable to identify them. So i've checked the sources of the Gecko layout engine, but i'm not sure where i can find this formula (in the sources).

It may be in layout/painting/nsCSSRenderingBorders.cpp, but there's still a lot of code and it's C++ (i've no skills into that language)

See Gecko repository : https://github.com/mozilla/gecko-dev

So, if anyone could help me in order to achieve this adaptation, or give me the blocks of code which are computing border-radius "arcs, orientation ?" (in gecko) i'll be able to do that.

Javascript/HTML snippet (current, close from good adaptation)

(RED SHAPE = Canvas shape, GREEN SHAPE = HTML shape)

I'm using this function to draw the shape : ctx.constructor.prototype.fillRoundedRect

And this function in order to get close from the browser adaptation : correctRadius

As you will see, im getting this result when TopLEFT, TopRight and BottomLEFT sliders are at max value. The browser (green) is rendering it perfectly, and mine is bad (red).

See snippet below

bad result js border radius css

// Ctx
var ctx = document.getElementById("rounded-rect").getContext("2d");

function correctRadius(r, w, h) {
  if (r.tl + r.tr > w) {
    r.tl -= (r.tl + r.tr - w) / 2;
    r.tr = w - r.tl;
  }
  if (r.bl + r.br > w) {
    r.br -= (r.br + r.bl - w) / 2;
    r.bl = w - r.br;
  }
  if (r.tl + r.bl > h) {
    r.tl -= (r.tl + r.bl - h) / 2;
    r.bl = h - r.tl;
  }
  if (r.tr + r.br > h) {
    r.tr -= (r.tr + r.br - h) / 2;
    r.br = h - r.tr;
  }
 }

//Round rect func
ctx.constructor.prototype.fillRoundedRect =
function (xx, yy, ww, hh, rad, fill, stroke) {
 correctRadius(rad, ww, hh);
 if (typeof(rad) === "undefined") rad = 5;
 this.beginPath();
 this.moveTo(xx, yy);
 this.arcTo(xx + ww, yy, xx + ww, yy + hh, rad.tr);
 this.arcTo(xx + ww, yy + hh, xx, yy + hh, rad.br);
 this.arcTo(xx, yy + hh, xx, yy, rad.bl);
 this.arcTo(xx, yy, xx + ww, yy, rad.tl);
 if (stroke) this.stroke();  // Default to no stroke
 if (fill || typeof(fill) === "undefined") this.fill();  // Default to fill
};

ctx.fillStyle = "red";
ctx.strokeStyle = "#ddf";

var copy = document.getElementById('copy');
var tl = document.getElementById('tl');
var tr = document.getElementById('tr');
var bl = document.getElementById('bl');
var br = document.getElementById('br');
var off = document.getElementById('off');

function test() {
ctx.clearRect(0, 0, 600, 500);


/* 1.Top left */
/* 2. Top right */
/* 3. Bottom right  */
/* 4. Bottom left */
var borders = [tl.value, tr.value, br.value, bl.value].join('px ') + 'px';

copy.style.borderRadius = borders;
var copyRad = borders.replace(/px/g, '').split(' ').map(function (a) {
 return parseInt(a)
});

var rad = {
 tl: copyRad[0],
 tr: copyRad[1],
 br: copyRad[2],
 bl: copyRad[3]
};
var o = +off.value;
ctx.fillRoundedRect(15 + o, 15 + o, 100, 100, rad);
}

tl.oninput = test;
tr.oninput = test;
bl.oninput = test;
br.oninput = test;
off.oninput = test;
test();
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        html, body {
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>


<div style="display:inline-block; position: absolute;
left:120px;top:120px; width: 100px; height: 100px; background:green;

border-radius: 10px 5px 10px 20px;" id="copy">

</div>

<canvas style="display: inline-block; position: absolute; zindex:0; left:0; top:0;" id="rounded-rect" width="600" height="500">

</canvas>


<div style="top: 300px; position:absolute; z-index: 1;">
    <label>
        Top left
        <input type="range" min="1" max="100" value="0" class="slider" id="tl"></label><br/>
    <label>
        Top right
        <input type="range" min="1" max="100" value="0" class="slider" id="tr"></label><br/>
    <label>
        Bottom left
        <input type="range" min="1" max="100" value="0" class="slider" id="bl"></label><br/>
    <label>
        Bottom right
        <input type="range" min="1" max="100" value="0" class="slider" id="br"></label><br/>
    <label>
        Offset
        <input type="range" min="1" max="200" value="0" class="slider" id="off"></label><br/>
</div>

</body>
</html>
doğukan
  • 23,073
  • 13
  • 57
  • 69
Backspace
  • 77
  • 7

1 Answers1

2

The issue is with your correctRadius function. As you can see you are re-calculating value more than once which is incorrect.

Let's take an example with TopLeft, TopRight and BottomLeft set at max values (100px):

1) You will consider the first condition and you will update your values like this:

TopLeft = 50px | TopRight = 50px (these are correct values)

2) Now you will consider the third condition as you have TL+TB (100px + 50px) > h (100px) and you will update the values like this:

TopLeft = 25px | BottomLeft 75px (these are wrong values and the correct ones should be 50px for both)

So you don't have to calculate values separately, you should consider all of them and do only one calculation for each side depending on initial values.


The idea is to consider the max value of the difference between two adjacent side. Let's consider the same example above and make 3 different values like this:

TL = 100px | TR = 90px | BL = 100px | BR = 0px

TL is adjacent to TR and BL:

  • Considering TR we will have 190px > 100px and thus TL = 45px
  • Considering BL we will have 200px > 100px and this TL = 50px

So we should consider the max value which is 50px. We do the same for the other sides.

Here is the full code:

// Ctx
var ctx = document.getElementById("rounded-rect").getContext("2d");

function correctRadius(r, w, h) {
  var tl=r.tl;
  var tr=r.tr;
  var br=r.br;
  var bl=r.bl;

  r.tl -= Math.max(Math.max((tl + tr - w)/2,0),
                   Math.max((tl + bl - h)/2,0));
                   
  r.tr -= Math.max(Math.max((tr + tl - w)/2,0),
                   Math.max((tr + br - h)/2,0));
                    
  r.br -= Math.max(Math.max((br + bl - w)/2,0),
                   Math.max((br + tr - h)/2,0));
                   
  r.bl -= Math.max(Math.max((bl + br - w)/2,0),
                   Math.max((bl + tl - h)/2,0));

 }

//Round rect func
ctx.constructor.prototype.fillRoundedRect =
function (xx, yy, ww, hh, rad, fill, stroke) {
 correctRadius(rad, ww, hh);
 if (typeof(rad) === "undefined") rad = 5;
 this.beginPath();
 this.moveTo(xx, yy);
 this.arcTo(xx + ww, yy, xx + ww, yy + hh, rad.tr);
 this.arcTo(xx + ww, yy + hh, xx, yy + hh, rad.br);
 this.arcTo(xx, yy + hh, xx, yy, rad.bl);
 this.arcTo(xx, yy, xx + ww, yy, rad.tl);
 if (stroke) this.stroke();  // Default to no stroke
 if (fill || typeof(fill) === "undefined") this.fill();  // Default to fill
};

ctx.fillStyle = "red";
ctx.strokeStyle = "#ddf";

var copy = document.getElementById('copy');
var tl = document.getElementById('tl');
var tr = document.getElementById('tr');
var bl = document.getElementById('bl');
var br = document.getElementById('br');
var off = document.getElementById('off');

function test() {
ctx.clearRect(0, 0, 600, 500);


/* 1.Top left */
/* 2. Top right */
/* 3. Bottom right  */
/* 4. Bottom left */
var borders = [tl.value, tr.value, br.value, bl.value].join('px ') + 'px';

copy.style.borderRadius = borders;
var copyRad = borders.replace(/px/g, '').split(' ').map(function (a) {
 return parseInt(a)
});

var rad = {
 tl: copyRad[0],
 tr: copyRad[1],
 br: copyRad[2],
 bl: copyRad[3]
};
var o = +off.value;
ctx.fillRoundedRect(15 + o, 15 + o, 100, 100, rad);
}

tl.oninput = test;
tr.oninput = test;
bl.oninput = test;
br.oninput = test;
off.oninput = test;
test();
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        html, body {
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>


<div style="display:inline-block; position: absolute;
left:120px;top:120px; width: 100px; height: 100px; background:green;

border-radius: 10px 5px 10px 20px;" id="copy">

</div>

<canvas style="display: inline-block; position: absolute; zindex:0; left:0; top:0;" id="rounded-rect" width="600" height="500">

</canvas>


<div style="top: 300px; position:absolute; z-index: 1;">
    <label>
        Top left
        <input type="range" min="1" max="100" value="0" class="slider" id="tl"></label><br/>
    <label>
        Top right
        <input type="range" min="1" max="100" value="0" class="slider" id="tr"></label><br/>
    <label>
        Bottom left
        <input type="range" min="1" max="100" value="0" class="slider" id="bl"></label><br/>
    <label>
        Bottom right
        <input type="range" min="1" max="100" value="0" class="slider" id="br"></label><br/>
    <label>
        Offset
        <input type="range" min="1" max="200" value="0" class="slider" id="off"></label><br/>
</div>

</body>
</html>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415