使用画布API

时间:2018-02-01 01:25:49

标签: javascript html css math canvas

我想将以下CSS形状重现为画布。它具有以下属性:

width: 325px;
height: 200px;
background: green;
border-radius: 60px 110px / 100px 80px;

the rendered shape

我已经有了制作圆角矩形的功能,但没有变形......请参阅相关的叠加帖子:https://stackoverflow.com/a/48491607/9264003

我已经尝试bezierCurveTo()& arcTo()功能,但没有成功。

我认为我们必须为每个角计算一个椭圆形:左上角,右上角,右下角,左下角,但我根本不确定......

如果有人要复制这个形状,或者给我任何提示或公式以便计算这将是一个非常好的开始!

2 个答案:

答案 0 :(得分:1)

2DContext API实际上有一个ellipse方法。它的浏览器支持不是很棒,但它可以是polyfilled

使用这种方法可以简化操作,因为我们可以用它在每个角落绘制四分之一的椭圆,但它并不能解决整个问题......

你仍然需要解析CSSString以了解如何绘制省略号,为此,我使用一个简单的虚拟div + getComputedStyle。
根据规格,如果两个border-XXX-XXX-radius计算值中的一个是0px,那么我们应绘制一个平方角。
你还需要考虑overlapping corners rule,为此,我借用了niklasvh的html2canvas implementation CSSWG算法。

这是我尝试为canvas创建这样的Border-radius函数,但是我没有进行大量的测试,所以它可能会失败。

另请注意,它只接受简写border-radius CSS语法,但如果您还希望传递长手的语法,则很容易调整。

var w = c.width = 325,
  h = c.height = 200,
  ctx = c.getContext('2d');
ctx.fillStyle = 'blue';

inp.onchange = function() {
  blurb.style.borderRadius = this.value;
  ctx.clearRect(0, 0, w, h);
  drawBorderRadius(ctx, this.value, w, h);
  ctx.fill();
};
inp.onchange();

function drawBorderRadius(ctx, CSSRule, w, h) {
  var radii = parseBorderRadiusRules(CSSRule);
  fixOverlappingCorners(radii);

  ctx.beginPath();
  var x, y, h_, v_;

  // top-left corner
  if (hasZero(radii.topleft))
    ctx.moveTo(0, 0);
  else {
    x = radii.topleft[0];
    y = radii.topleft[1];
    ctx.ellipse(x, y, x, y, 0, Math.PI, Math.PI * 1.5);
  }
  // top-right corner
  if (hasZero(radii.topright))
    ctx.lineTo(w, 0);
  else {
    x = radii.topright[0];
    y = radii.topright[1];
    ctx.ellipse(w - radii.topright[0], y, x, y, 0, -Math.PI / 2, 0);
  }
  //bottom-right corner
  if (hasZero(radii.bottomright))
    ctx.lineTo(w, h);
  else {
    x = radii.bottomright[0];
    y = radii.bottomright[1];
    ctx.ellipse(w - x, h - y, x, y, 0, 0, Math.PI / 2);
  }
  //bottom-left corner
  if (hasZero(radii.bottomleft))
    ctx.lineTo(0, h);
  else {
    x = radii.bottomleft[0];
    y = radii.bottomleft[1];
    ctx.ellipse(x, h - y, x, y, 0, Math.PI / 2, Math.PI);
  }

  // we need to check if one value is zero in order to draw a squared corner in such case
  function hasZero(corner) {
    return !Array.isArray(corner) ||
      corner.indexOf(0) > -1 ||
      corner.indexOf(NaN) > -1;
  }
  // returns a dictionnary of four corners [horizontal, vertical] values as px
  function parseBorderRadiusRules(CSSstring) {
    var elem = document.createElement('div');
    elem.style.borderRadius = CSSstring;
    elem.style.width = w;
    elem.style.height = h;
    elem.style.position = 'absolute';
    elem.zIndex = -999;
    document.body.appendChild(elem);
    var computed = getComputedStyle(elem);
    var radii = {
      topleft: cleanRule(computed['border-top-left-radius']),
      topright: cleanRule(computed['border-top-right-radius']),
      bottomright: cleanRule(computed['border-bottom-right-radius']),
      bottomleft: cleanRule(computed['border-bottom-left-radius'])
    };
    document.body.removeChild(elem);

    return radii;

    function cleanRule(str) {
      var xy = str.split(' ');
      if (xy.length === 1) {
        xy[1] = xy[0];
      }
      return xy.map(toPx);
    }

    function toPx(str, index) {
      var val = parseFloat(str);
      if (str.indexOf('%') > -1) {
        return percentageToPx(val, !index ? w : h);
      }
      return val;
    }

    function percentageToPx(percent, length) {
      return length * (percent / 100);
    }
  }
  // borrowed from https://github.com/niklasvh/html2canvas/blob/8788a9f458f538c004a626c5ce7ee24b53e48c1c/src/Bounds.js#L200
  // https://github.com/niklasvh/html2canvas/blob/master/LICENSE
  function fixOverlappingCorners(radii) {
    var factors = [
        w / (radii.topleft[0] + radii.topright[0]),
        w / (radii.bottomleft[0] + radii.bottomright[0]),
        h / (radii.topleft[1] + radii.bottomleft[1]),
        h / (radii.topright[1] + radii.bottomright[1])
      ],
      minFactor = Math.min.apply(null, factors);
      
    if (minFactor <= 1) {
      for (var key in radii) {
        radii[key] = radii[key].map(scale);
      }
    }

    function scale(value) {
      return value * minFactor;
    }
  }
}
#blurb {
  width: 325px;
  height: 200px;
  background: green;
}

input:checked+.cont>#blurb {
  opacity: 0.5;
  position: absolute;
  background: red;
}

.cont {
  position: relative;
}
<input id="inp" value="60px 110px / 100px 80px">
<label>overlap</label><input type="checkbox">
<div class="cont">
  <div id="blurb"></div>
  <canvas id="c"></canvas>
</div>

答案 1 :(得分:0)

这里是一个SVG,使用弧段:

<path d="M 60,0 225,0 A 100,80 0 0 1 325,80 L 325,90 A 60,110 0 0 1 265,200 L 100,200 A 100,80 0 0 1 0,120 L 0,110 A 60,110 0 0 1 60,0 Z" />

Inkscape中的屏幕截图:

Screenshot

根据您尝试做的事情,最简单的方法就是使用SVG并忘记画布。

但是如果你真的需要这个在画布上,你可以用弧形建相同的东西,但它更棘手,因为arcTo只做圆弧,而不是椭圆弧。

解决这个问题的一种方法是在绘制每个弧之前更改绘图上下文的缩放比例。

以下是一个例子:

&#13;
&#13;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

function ellipticalArcTo(x1, y1, x2, y2, rx, ry) {
  ctx.save();
  const xScale = 1;
  const yScale = ry / rx;
  ctx.scale(xScale, yScale);
  ctx.arcTo(x1 / xScale, y1 / yScale, x2 / xScale, y2 / yScale, rx);
  ctx.restore();
}

ctx.fillStyle = 'green';
ctx.beginPath();

ctx.moveTo(60, 0);
ctx.lineTo(225, 0);
ellipticalArcTo(325, 0, 325, 80, 100, 80);
ctx.lineTo(325, 90);
ellipticalArcTo(325, 200, 215, 200, 60, 110);
ctx.lineTo(100, 200);
ellipticalArcTo(0, 200, 0, 120, 100, 80);
ctx.lineTo(0, 110);
ellipticalArcTo(0, 0, 60, 0, 60, 110);

ctx.fill();
&#13;
<canvas id="canvas" width="400" height="300"></canvas>
&#13;
&#13;
&#13;

请注意,我还没有使用任何旋转弧进行测试。据我所知,当主轴不是水平和垂直时,它会被打破。但是你不需要这个来模拟矩形的边界半径。