Javascript - Gecko border radius adaptation on HTM

2019-05-08 03:40发布

问题:

I'm trying to figure out how to reproduce the behavior of the "border-radius" css property into an HTML canvas.

So i've already done something in Javascript in order to compute the correct borders of a given shape, using a specific radius (for each corner).

Here is the previous question if needed : Gecko - CSS layout border radius - Javascript conversion

I've managed to get close from the browser adaptation but there's still an issue, and it seems that it's the last, and the hard part!

Let's take an example in order to explain you the problem

Take a shape of 100px width & 100px height. Now apply the following radius to each corner :

  • Top left : 37px
  • Top right : 100px
  • Bottom right : 1px
  • Bottom left : 100px

So the css style for this will be border-radius : 37px 100px 1px 100px

Now let's consider using the code below in order to draw this shape in a canvas.

By using the function correctRadius(r, w, h) in order to avoid bad shapes, each corners will be computed like that :

=> {tl: 18.5, tr: 81.5, br: 0.5, bl: 81.5}

Here is a visual of this :

You will be able to test it in the following snippet

As you can see, the browser shape (green) is overlapped with the canvas shape (brown + 'pink' due to opacity). I've put some opacity on it in order to check the bad corners (pink).

The brown shape do not fit properly into the green shape, the top left corner is getting out of the base, and the bottom left and top right corners don't fit the green shape.

I've already tried to fix that but with no success, and also looked the sources of the Gecko layout engine (https://github.com/mozilla/gecko-dev) in this file : layout/painting/nsCSSRenderingBorders.cpp, but haven't found anything + i have no skills in C++


If anyone could help me to figure this out, or give me some advice, i'll be able to fix this and get the borders working

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

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 last = [];
setInterval(function() {


  /* 1.Top left */
  /* 2. Top right */
  /* 3. Bottom right  */
  /* 4. Bottom left */

  var bordersCSSProps = [
      "border-top-left-radius",
      "border-top-right-radius",
      "border-bottom-right-radius",
      "border-bottom-left-radius"
    ],
    elementBorders = [],
    elementStyle = getComputedStyle(copy);

  var changed = false;

  for (var i = 0; i < 4; ++i) {
    elementBorders[i] = elementStyle.getPropertyValue(bordersCSSProps[i]);
    if (elementBorders[i] !== last[i]) {
      changed = true;
      last[i] = elementBorders[i];
    }
  }

  if (changed) {

    var borders = [].concat(elementBorders).map(function(a) {
      return parseInt(a)
    });
    var rad = {
      tl: borders[0],
      tr: borders[1],
      br: borders[2],
      bl: borders[3]
    };

    ctx.clearRect(0, 0, 600, 500);


    ctx.fillRoundedRect(120, 120, 100, 100, rad);


  }
}, 1E3 / 60);


function elemBordersSet() {
  var borders = [tl.value, tr.value, br.value, bl.value].join('px ') + 'px';
  copy.style.borderRadius = borders;

}

tl.oninput = elemBordersSet;
tr.oninput = elemBordersSet;
bl.oninput = elemBordersSet;
br.oninput = elemBordersSet;
html,
body {
  margin: 0;
  padding: 0;
}
<div style="display:inline-block; position: absolute;
left:120px;top:120px; width: 100px; height: 100px; background:green;

border-radius:  100px 49px 1px 1px;" id="copy">

</div>

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

</canvas>


<div style="margin-top:250px; position:absolute; z-index:5">
  <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/>
</div>

回答1:

Found the solution !

We just need to compute a scaleRatio :

First, we get the maxRadiusWidth (r.tl + r.tr, r.bl + r.br) & maxRadiusHeight (r.tl + r.bl, r.tr + r.br)

and then a widthRatio = (w / maxRadiusWidth) & heightRatio = (h / maxRadiusHeight) with the size of the shape (WIDTH & HEIGHT)

Then we take the lower of theses two variables : Math.min(Math.min(widthRatio, heightRatio), 1) in order to not get out of the shape & we make sure that the ratio is below 1.

Finally, we simply need to multiply each corner by this ratio in order to get the correct size !

r.tl = tl*scaleRatio;
r.tr = tr*scaleRatio;
r.br = br*scaleRatio;
r.bl = bl*scaleRatio;

See snippet below ;)

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

function correctRadius(r, w, h) {

        var
            maxRadiusWidth = Math.max(r.tl + r.tr, r.bl + r.br),
            maxRadiusHeight = Math.max(r.tl + r.bl, r.tr + r.br),
            widthRatio = w / maxRadiusWidth,
            heightRatio = h / maxRadiusHeight,
            scaleRatio = Math.min(Math.min(widthRatio, heightRatio), 1);


        for (var k in r)
            r[k] = r[k] * scaleRatio;

}


//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 last = [];
setInterval(function() {


    /* 1.Top left */
    /* 2. Top right */
    /* 3. Bottom right  */
    /* 4. Bottom left */

    var bordersCSSProps = [
            "border-top-left-radius",
            "border-top-right-radius",
            "border-bottom-right-radius",
            "border-bottom-left-radius"
        ],
        elementBorders = [],
        elementStyle = getComputedStyle(copy);

    var changed = false;

    for (var i = 0; i < 4; ++i) {
        elementBorders[i] = elementStyle.getPropertyValue(bordersCSSProps[i]);
        if (elementBorders[i] !== last[i]) {
            changed = true;
            last[i] = elementBorders[i];
        }
    }

    if (changed) {

        var borders = [].concat(elementBorders).map(function(a) {
            return parseInt(a)
        });
        var rad = {
            tl: borders[0],
            tr: borders[1],
            br: borders[2],
            bl: borders[3]
        };

        ctx.clearRect(0, 0, 600, 500);


        ctx.fillRoundedRect(120, 120, 100, 200, rad);


    }
}, 1E3 / 60);


function elemBordersSet() {
    var borders = [tl.value, tr.value, br.value, bl.value].join('px ') + 'px';
    copy.style.borderRadius = borders;

}

tl.oninput = elemBordersSet;
tr.oninput = elemBordersSet;
bl.oninput = elemBordersSet;
br.oninput = elemBordersSet;
<div style="display:inline-block; position: absolute;
left:120px;top:120px; width: 100px; height: 200px; background:green;

border-radius: 33px 71px 40px 100px;" id="copy">

</div>

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

</canvas>


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