如何使用HTML Canvas绘制圆角矩形?

200

HTML画布提供了绘制矩形的方法:fillRect()strokeRect(),但我找不到用于制作圆角矩形的方法。我该怎么做?


4
这个人介绍了如何以一种不错的方式制作带有边框和填充的圆角矩形:http://js-bits.blogspot.com/2010/07/canvas-rounded-corner-rectangles.html - nerdess
https://dev59.com/OaPia4cB1Zd3GeqPsg_T#44856925 - jedierikb
15个回答

436
现在你可以直接使用context.roundRect。更多详细信息请参考Kaiido的回答

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

// Draw using 5px for border radius on all sides
// stroke it but no fill
ctx.roundRect(5, 5, 50, 50, 5);
ctx.stroke();

// To change the color on the rectangle, just manipulate the context
ctx.strokeStyle = "rgb(255, 0, 0)";
ctx.fillStyle = "rgba(255, 255, 0, .5)";
ctx.beginPath();
ctx.roundRect(100, 5, 100, 100, 20);
ctx.stroke();
ctx.fill();

// Manipulate it again
ctx.strokeStyle = "#0f0";
ctx.fillStyle = "#ddd";
// Different radii for each corner, top-left clockwise to bottom-left
ctx.beginPath();
ctx.roundRect(300, 5, 200, 100, [50,0,25,0]);
ctx.fill();
ctx.stroke();
<canvas id="rounded-rect" width="500" height="200">
  <!-- Insert fallback content here -->
</canvas>


不支持roundRect的旧浏览器的解决方案

截至2023年4月10日,所有主要浏览器在最新版本中都支持它。

请参阅https://caniuse.com/mdn-api_canvasrenderingcontext2d_roundrect


我也需要同样的东西,所以我为此创建了一个函数。

/**
 * Draws a rounded rectangle using the current state of the canvas.
 * If you omit the last three params, it will draw a rectangle
 * outline with a 5 pixel border radius
 * @param {CanvasRenderingContext2D} ctx
 * @param {Number} x The top left x coordinate
 * @param {Number} y The top left y coordinate
 * @param {Number} width The width of the rectangle
 * @param {Number} height The height of the rectangle
 * @param {Number} [radius = 5] The corner radius; It can also be an object 
 *                 to specify different radii for corners
 * @param {Number} [radius.tl = 0] Top left
 * @param {Number} [radius.tr = 0] Top right
 * @param {Number} [radius.br = 0] Bottom right
 * @param {Number} [radius.bl = 0] Bottom left
 * @param {Boolean} [fill = false] Whether to fill the rectangle.
 * @param {Boolean} [stroke = true] Whether to stroke the rectangle.
 */
function roundRect(
  ctx,
  x,
  y,
  width,
  height,
  radius = 5,
  fill = false,
  stroke = true
) {
  if (typeof radius === 'number') {
    radius = {tl: radius, tr: radius, br: radius, bl: radius};
  } else {
    radius = {...{tl: 0, tr: 0, br: 0, bl: 0}, ...radius};
  }
  ctx.beginPath();
  ctx.moveTo(x + radius.tl, y);
  ctx.lineTo(x + width - radius.tr, y);
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
  ctx.lineTo(x + width, y + height - radius.br);
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
  ctx.lineTo(x + radius.bl, y + height);
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
  ctx.lineTo(x, y + radius.tl);
  ctx.quadraticCurveTo(x, y, x + radius.tl, y);
  ctx.closePath();
  if (fill) {
    ctx.fill();
  }
  if (stroke) {
    ctx.stroke();
  }
}

// Now you can just call
var ctx = document.getElementById("rounded-rect").getContext("2d");
// Draw using default border radius, 
// stroke it but no fill (function's default values)
roundRect(ctx, 5, 5, 50, 50);
// To change the color on the rectangle, just manipulate the context
ctx.strokeStyle = "rgb(255, 0, 0)";
ctx.fillStyle = "rgba(255, 255, 0, .5)";
roundRect(ctx, 100, 5, 100, 100, 20, true);
// Manipulate it again
ctx.strokeStyle = "#0f0";
ctx.fillStyle = "#ddd";
// Different radii for each corner, others default to 0
roundRect(ctx, 300, 5, 200, 100, {
  tl: 50,
  br: 25
}, true);
<canvas id="rounded-rect" width="500" height="200">
  <!-- Insert fallback content here -->
</canvas>

Corgalore提供了每个角落的不同半径。 请参阅http://js-bits.blogspot.com/2010/07/canvas-rounded-corner-rectangles.html以获取更详细的解释。

9
完美的回答...为什么这还不是Canvas的本地功能呢?谢谢。 - andygoestohollywood
2
代码有一个错误,它需要在填充之后进行描边,否则在小矩形上,填充将覆盖描边。 - Zig Mandel
2
我手头没有这个例子,但我不得不修改那个订单,因为我在我的代码中测试了一个案例。这是合乎逻辑的,如果你还没有填充矩形,它怎么能正确地描绘(使用矩形背景颜色进行平滑处理)? - Zig Mandel
2
@Juan,抱歉我的错,我注意到博客文章后发现了那个细节。我本意是要撤销编辑。干得好,+1 给你! - MrPizzaFace
6
Zig Mandel是正确的:应该先填充再描边。原因在于,如果你先描边再填充,那么线宽会减半。试着用非常粗的线宽(比如20),将一个用背景颜色填充的圆角矩形与一个未填充的圆角矩形进行比较。填充的那个矩形的线宽将是未填充的线宽的一半。 - Andrew Stacey
显示剩余16条评论

143

我开始使用@jhoff的解决方案,但重写它以使用宽度/高度参数,并使用arcTo使代码更加简洁:

CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
  if (w < 2 * r) r = w / 2;
  if (h < 2 * r) r = h / 2;
  this.beginPath();
  this.moveTo(x+r, y);
  this.arcTo(x+w, y,   x+w, y+h, r);
  this.arcTo(x+w, y+h, x,   y+h, r);
  this.arcTo(x,   y+h, x,   y,   r);
  this.arcTo(x,   y,   x+w, y,   r);
  this.closePath();
  return this;
}

还可以返回上下文,这样你就可以进行链式操作,例如:

ctx.roundRect(35, 10, 225, 110, 20).stroke(); //or .fill() for a filled rect

5
除了那个好的解决方案,我不会碰Canvas渲染上下文。 - Ash Blue
1
这是一个居中的矩形,如果有人需要一个其左上角在(x,y)的矩形,则保存上下文,添加平移(-w/2,-h/2),然后恢复上下文。 - nessa.gp
谢谢,这是目前为止唯一有效的方法,其他方法在半径大于高度或宽度时会出现问题。已实施! - Howzieky
1
请注意,此解决方案适用于使任何多边形具有圆角。一个示例链接:fiddle - Doguleez
1
@nessa.gp 这不是居中的,你可能误读了前两行。 - agiopnl
显示剩余4条评论

56

由于某些原因,我似乎在Firefox 3.5和Opera 10.0中使用arcTo时遇到了问题。类似于此网站:http://ditchnet.org/canvas/CanvasRoundedCornerExample.html - bgw
arcTo在最新版本的FF中已经修复。 - Ash Blue
7
可以举个例子吗? - Jean-Pierre Bécotte
截至2022年,可能很快或以后会有一个规范。请参见Woold answer - lepe

35

Juan,我稍微改进了你的方法,以允许单独更改每个矩形角的半径:

/** 
 * Draws a rounded rectangle using the current state of the canvas.  
 * If you omit the last three params, it will draw a rectangle  
 * outline with a 5 pixel border radius  
 * @param {Number} x The top left x coordinate 
 * @param {Number} y The top left y coordinate  
 * @param {Number} width The width of the rectangle  
 * @param {Number} height The height of the rectangle 
 * @param {Object} radius All corner radii. Defaults to 0,0,0,0; 
 * @param {Boolean} fill Whether to fill the rectangle. Defaults to false. 
 * @param {Boolean} stroke Whether to stroke the rectangle. Defaults to true. 
 */
CanvasRenderingContext2D.prototype.roundRect = function (x, y, width, height, radius, fill, stroke) {
    var cornerRadius = { upperLeft: 0, upperRight: 0, lowerLeft: 0, lowerRight: 0 };
    if (typeof stroke == "undefined") {
        stroke = true;
    }
    if (typeof radius === "object") {
        for (var side in radius) {
            cornerRadius[side] = radius[side];
        }
    }

    this.beginPath();
    this.moveTo(x + cornerRadius.upperLeft, y);
    this.lineTo(x + width - cornerRadius.upperRight, y);
    this.quadraticCurveTo(x + width, y, x + width, y + cornerRadius.upperRight);
    this.lineTo(x + width, y + height - cornerRadius.lowerRight);
    this.quadraticCurveTo(x + width, y + height, x + width - cornerRadius.lowerRight, y + height);
    this.lineTo(x + cornerRadius.lowerLeft, y + height);
    this.quadraticCurveTo(x, y + height, x, y + height - cornerRadius.lowerLeft);
    this.lineTo(x, y + cornerRadius.upperLeft);
    this.quadraticCurveTo(x, y, x + cornerRadius.upperLeft, y);
    this.closePath();
    if (stroke) {
        this.stroke();
    }
    if (fill) {
        this.fill();
    }
}

使用方法如下:

var canvas = document.getElementById("canvas");
var c = canvas.getContext("2d");
c.fillStyle = "blue";
c.roundRect(50, 100, 50, 100, {upperLeft:10,upperRight:10}, true, true);

2
这种方法可以对圆角进行非常精细的控制。为什么它不是被接受的答案呢? - vighnesh153
2
@VighneshRaut 可能是因为这个答案复制/粘贴了原始的被接受的答案并添加了圆角。我将其合并到被接受的答案中,并给予了这个答案的认可。被接受的答案有一个实时示例,如果您确实希望所有角落具有相同的半径(这是最常见的情况),则语法更简单。最后,这个答案建议修改本地对象的原型,这是不可取的。 - Ruan Mendes

26
大家好,有个好消息! roundRect(x, y, width, height, radii); 现在正式成为 Canvas 2D API 的一部分。
它可以在 CanvasRenderingContext2D、Path2D 和 OffscreenCanvasRenderingContext2D 对象上使用。
它的 radii 参数可以是以下几种形式之一:
- 单个浮点数,表示四个角的半径相同; - 包含单个浮点数的数组; - 两个浮点数,分别表示左上角+右下角和右上角+左下角的半径; - 三个浮点数,分别表示左上角、右上角+左下角和右下角的半径; - 四个浮点数,每个角一个半径; - 或者以上任意组合,但使用的是具有浮点数属性 xyDOMPointInit 对象,表示每个角的 x 半径和 y 半径,允许使用椭圆弧而不仅仅是圆弧。

现在已在所有主要浏览器中得到支持,对于旧版本的浏览器,您可以在此存储库中找到我制作的polyfill。

const canvas = document.querySelector("canvas");

const ctx = canvas.getContext("2d");
ctx.roundRect(20,20,80,80,[new DOMPoint(60,80), new DOMPoint(110,100)]);
ctx.strokeStyle = "green";
ctx.stroke();

const path = new Path2D();
path.roundRect(120,30,60,90,[0,25,new DOMPoint(60,80), new DOMPoint(110,100)]);
ctx.fillStyle = "purple";
ctx.fill(path);

// and a simple one
ctx.beginPath();
ctx.roundRect(200,20,80,80,[10]);
ctx.fillStyle = "orange";
ctx.fill();
<script src="https://cdn.jsdelivr.net/gh/Kaiido/roundRect@main/roundRect.js"></script>
<canvas></canvas>


1
这会发生吗? - swisswiss
1
@swisswiss,这已经发生了。这是规格的一部分,Chrome已经(部分)支持了。 - Kaiido
@Kaiido 我找不到这个在哪个版本的chrome中被支持,甚至在MDN上也没有记录。 - Coding Edgar
1
@CodingEdgar 哦,看起来他们仍然将其隐藏在实验性Web平台标志下。对于MDN,它已准备好上传到https://github.com/fserb/canvas2D/pull/18/files#diff-cb4343e566a25a1b9180e945317acd744715cf048961b61434d375198bfb58e3。 - Kaiido
Firefox在2022年的最后一天不支持它。 - Jay Wang
1
@JayWong 是的,答案里有: “目前只有Chrome有可用的实现,但是你可以在这个仓库中找到我制作的polyfill。” 当浏览器支持发生变化时,我会进行更新。 - Kaiido

16

下面的drawPolygon函数可用于绘制带有圆角的任何多边形。

在这里运行它。

function drawPolygon(ctx, pts, radius) {
  if (radius > 0) {
    pts = getRoundedPoints(pts, radius);
  }
  var i, pt, len = pts.length;
  ctx.beginPath();
  for (i = 0; i < len; i++) {
    pt = pts[i];
    if (i == 0) {          
      ctx.moveTo(pt[0], pt[1]);
    } else {
      ctx.lineTo(pt[0], pt[1]);
    }
    if (radius > 0) {
      ctx.quadraticCurveTo(pt[2], pt[3], pt[4], pt[5]);
    }
  }
  ctx.closePath();
}

function getRoundedPoints(pts, radius) {
  var i1, i2, i3, p1, p2, p3, prevPt, nextPt,
      len = pts.length,
      res = new Array(len);
  for (i2 = 0; i2 < len; i2++) {
    i1 = i2-1;
    i3 = i2+1;
    if (i1 < 0) {
      i1 = len - 1;
    }
    if (i3 == len) {
      i3 = 0;
    }
    p1 = pts[i1];
    p2 = pts[i2];
    p3 = pts[i3];
    prevPt = getRoundedPoint(p1[0], p1[1], p2[0], p2[1], radius, false);
    nextPt = getRoundedPoint(p2[0], p2[1], p3[0], p3[1], radius, true);
    res[i2] = [prevPt[0], prevPt[1], p2[0], p2[1], nextPt[0], nextPt[1]];
  }
  return res;
};

function getRoundedPoint(x1, y1, x2, y2, radius, first) {
  var total = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)),
      idx = first ? radius / total : (total - radius) / total;
  return [x1 + (idx * (x2 - x1)), y1 + (idx * (y2 - y1))];
};

该函数接收一个包含多边形点的数组,如下所示:

var canvas = document.getElementById("cv");
var ctx = canvas.getContext("2d");
ctx.strokeStyle = "#000000";
ctx.lineWidth = 5;

drawPolygon(ctx, [[20,   20],
                  [120,  20],
                  [120, 120],
                  [ 20, 120]], 10);
ctx.stroke();

这是一个端口和更通用版本的解决方案,发布在这里


16
这段代码创建了一个100像素的正方形,并给它的四个角设置了30像素的圆角。
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(100,100);
ctx.arcTo(0,100,0,0,30);
ctx.arcTo(0,0,100,0,30);
ctx.arcTo(100,0,100,100,30);
ctx.arcTo(100,100,0,100,30);
ctx.fill();


这正是我正在寻找的。 - Daniel Fanica
终于有一个简短而全面的回复,真正起作用了。谢谢。 - Jan Wirth

13

这是我写的一个程序...它使用弧线代替二次曲线,以更好地控制半径。此外,它将描边和填充留给您自己处理。

/* Canvas 2d context - roundRect
 *
 * Accepts 5 parameters:
     the start_x, 
     start_y points, 
     the end_x,
     end_y points, 
     the radius of the corners
 * 
 * No return value
 */

CanvasRenderingContext2D.prototype.roundRect = function(sx,sy,ex,ey,r) {
    var r2d = Math.PI/180;
    if( ( ex - sx ) - ( 2 * r ) < 0 ) { r = ( ( ex - sx ) / 2 ); } //ensure that the radius isn't too large for x
    if( ( ey - sy ) - ( 2 * r ) < 0 ) { r = ( ( ey - sy ) / 2 ); } //ensure that the radius isn't too large for y
    this.beginPath();
    this.moveTo(sx+r,sy);
    this.lineTo(ex-r,sy);
    this.arc(ex-r,sy+r,r,r2d*270,r2d*360,false);
    this.lineTo(ex,ey-r);
    this.arc(ex-r,ey-r,r,r2d*0,r2d*90,false);
    this.lineTo(sx+r,ey);
    this.arc(sx+r,ey-r,r,r2d*90,r2d*180,false);
    this.lineTo(sx,sy+r);
    this.arc(sx+r,sy+r,r,r2d*180,r2d*270,false);
    this.closePath();
}

这里有一个例子:

var _e = document.getElementById('#my_canvas');
var _cxt = _e.getContext("2d");
_cxt.roundRect(35,10,260,120,20);
_cxt.strokeStyle = "#000";
_cxt.stroke();

这样做如何让您更好地控制半径?我以为您要允许 x/y 半径(椭圆角),并且还可以为每个角指定不同的半径。 - Ruan Mendes
3
你的 r2d 可能希望被称为 d2r - Grumdrig
2
@JuanMendes:在这个解决方案中,(基于弧形的)圆角的形状比您的(基于二次曲线的)解决方案更加圆形。我认为这就是他所说的“对半径的更好控制”。 - Brent Bradburn
我也使用过这种方法,它比使用二次曲线更好。但是,如果你要绘制比矩形更复杂的图形,那么就会非常麻烦。希望能像Android画布中那样有一种自动方法。 - Aleksei Petrenko

11

所以这基于使用lineJoin="round",通过正确的比例、数学和逻辑,我已经能够做出这个函数。虽然不是完美的,但希望它能有所帮助。如果你想让每个角有不同的半径,请查看:https://p5js.org/reference/#/p5/rect

给你:

CanvasRenderingContext2D.prototype.roundRect = function (x,y,width,height,radius) {
    radius = Math.min(Math.max(width-1,1),Math.max(height-1,1),radius);
    var rectX = x;
    var rectY = y;
    var rectWidth = width;
    var rectHeight = height;
    var cornerRadius = radius;

    this.lineJoin = "round";
    this.lineWidth = cornerRadius;
    this.strokeRect(rectX+(cornerRadius/2), rectY+(cornerRadius/2), rectWidth-cornerRadius, rectHeight-cornerRadius);
    this.fillRect(rectX+(cornerRadius/2), rectY+(cornerRadius/2), rectWidth-cornerRadius, rectHeight-cornerRadius);
    this.stroke();
    this.fill();
}

CanvasRenderingContext2D.prototype.roundRect = function (x,y,width,height,radius) {
    radius = Math.min(Math.max(width-1,1),Math.max(height-1,1),radius);
    var rectX = x;
    var rectY = y;
    var rectWidth = width;
    var rectHeight = height;
    var cornerRadius = radius;

    this.lineJoin = "round";
    this.lineWidth = cornerRadius;
    this.strokeRect(rectX+(cornerRadius/2), rectY+(cornerRadius/2), rectWidth-cornerRadius, rectHeight-cornerRadius);
    this.fillRect(rectX+(cornerRadius/2), rectY+(cornerRadius/2), rectWidth-cornerRadius, rectHeight-cornerRadius);
    this.stroke();
    this.fill();
}
    var canvas = document.getElementById("myCanvas");
    var ctx = canvas.getContext('2d');
function yop() {
  ctx.clearRect(0,0,1000,1000)
  ctx.fillStyle = "#ff0000";
  ctx.strokeStyle = "#ff0000";  ctx.roundRect(Number(document.getElementById("myRange1").value),Number(document.getElementById("myRange2").value),Number(document.getElementById("myRange3").value),Number(document.getElementById("myRange4").value),Number(document.getElementById("myRange5").value));
requestAnimationFrame(yop);
}
requestAnimationFrame(yop);
<input type="range" min="0" max="1000" value="10" class="slider" id="myRange1"><input type="range" min="0" max="1000" value="10" class="slider" id="myRange2"><input type="range" min="0" max="1000" value="200" class="slider" id="myRange3"><input type="range" min="0" max="1000" value="100" class="slider" id="myRange4"><input type="range" min="1" max="1000" value="50" class="slider" id="myRange5">
<canvas id="myCanvas" width="1000" height="1000">
</canvas>


1
欢迎来到StackOverflow!由于这段代码可能解决问题,因此最好添加更多关于它如何工作的说明。 - Tân

5
这里有一个使用lineJoin属性来圆角的解决方案。如果你只需要一个实心形状,它就有效,但如果你需要比边框半径小的细边框,则效果不佳。

function roundedRect(ctx, options) {
    ctx.strokeStyle = options.color;
    ctx.fillStyle = options.color;
    ctx.lineJoin = "round";
    ctx.lineWidth = options.radius;

    ctx.strokeRect(
        options.x+(options.radius*.5),
        options.y+(options.radius*.5),
        options.width-options.radius,
        options.height-options.radius
    );

    ctx.fillRect(
        options.x+(options.radius*.5),
        options.y+(options.radius*.5),
        options.width-options.radius,
        options.height-options.radius
    );

    ctx.stroke();
    ctx.fill();
}

const canvas = document.getElementsByTagName("canvas")[0];
const ctx = canvas.getContext("2d");

roundedRect(ctx, {
    x: 10,
    y: 10,
    width: 200,
    height: 100,
    radius: 35,
    color: "red"
});
<canvas></canvas>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接