HTML Canvas provides methods for drawing rectangles, fillRect()
and strokeRect()
, but I can't find a method for making rectangles with rounded corners. How can I do that?
I needed to do the same thing and created a method to do it.
Updated to use ES6
/** * 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);
Different radii per corner provided by Corgalore
See http://js-bits.blogspot.com/2010/07/canvas-rounded-corner-rectangles.html for further explanation
I started with @jhoff's solution, but rewrote it to use width/height parameters, and using arcTo
makes it quite a bit more terse:
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;
}
Also returning the context so you can chain a little. E.g.:
ctx.roundRect(35, 10, 225, 110, 20).stroke(); //or .fill() for a filled rect
(x,y)
, save the context, add a translation to (-w/2,-h/2)
, and restore the context.
The HTML5 canvas doesn't provide a method to draw a rectangle with rounded corners.
How about using the lineTo()
and arc()
methods?
You can also use the quadraticCurveTo()
method instead of the arc()
method.
Juan, I made a slight improvement to your method to allow for changing each rectangle corner radius individually:
/**
* 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();
}
}
Use it like this:
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);
This code creates a 100-pixel square, with rounded corners of 30 pixels.
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();
The drawPolygon
function below can be used to draw any polygon with rounded corners.
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))];
};
The function receives an array with the polygon points, like this:
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();
This is a port and a more generic version of a solution posted here.
Good news everyone!
roundRect(x, y, width, height, radii); is now officially part of the Canvas 2D API.
It is exposed on CanvasRenderingContext2D, Path2D and OffscreenCanvasRenderingContext2D objects.
Its radii
parameter is an Array which contains either
a single float, representing the radius to use for all four corners,
two floats, for the top-left + bottom-right and top-right + bottom-left corners respectively,
three floats, for the top-left, top-right + bottom-left and bottom-right respectively,
or four floats, one per corner,
OR the same combinations, but with a DOMPointInit object, representing the x-radius and y-radius of each corner.
Currently, only Chrome has an implementation available, but you can find a polyfill I made, in this repo.
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();
Here's one I wrote... uses arcs instead of quadratic curves for better control over radius. Also, it leaves the stroking and filling up to you
/* 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();
}
Here is an example:
var _e = document.getElementById('#my_canvas');
var _cxt = _e.getContext("2d");
_cxt.roundRect(35,10,260,120,20);
_cxt.strokeStyle = "#000";
_cxt.stroke();
r2d
probably wants to be called d2r
.
So this is based out of using lineJoin="round" and with the proper proportions, mathematics and logic I have been able to make this function, this is not perfect but hope it helps. If you want to make each corner have a different radius take a look at: https://p5js.org/reference/#/p5/rect
Here ya go:
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);
Opera, ffs.
if (window["CanvasRenderingContext2D"]) {
/** @expose */
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();
if (r < 1) {
this.rect(x, y, w, h);
} else {
if (window["opera"]) {
this.moveTo(x+r, y);
this.arcTo(x+r, y, x, y+r, r);
this.lineTo(x, y+h-r);
this.arcTo(x, y+h-r, x+r, y+h, r);
this.lineTo(x+w-r, y+h);
this.arcTo(x+w-r, y+h, x+w, y+h-r, r);
this.lineTo(x+w, y+r);
this.arcTo(x+w, y+r, x+w-r, y, r);
} else {
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();
};
/** @expose */
CanvasRenderingContext2D.prototype.fillRoundRect = function(x, y, w, h, r) {
this.roundRect(x, y, w, h, r);
this.fill();
};
/** @expose */
CanvasRenderingContext2D.prototype.strokeRoundRect = function(x, y, w, h, r) {
this.roundRect(x, y, w, h, r);
this.stroke();
};
}
Since Opera is going WebKit, this should also remain valid in the legacy case.
Here's a solution using the lineJoin
property to round the corners. It works if you just need a solid shape, but not so much if you need a thin border that's smaller than the border radius.
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" });
To make the function more consistent with the normal means of using a canvas context, the canvas context class can be extended to include a 'fillRoundedRect
' method -- that can be called in the same way fillRect
is called:
var canv = document.createElement("canvas");
var cctx = canv.getContext("2d");
// If thie canvasContext class doesn't have a fillRoundedRect, extend it now
if (!cctx.constructor.prototype.fillRoundedRect) {
// Extend the canvaseContext class with a fillRoundedRect method
cctx.constructor.prototype.fillRoundedRect =
function (xx,yy, ww,hh, rad, fill, stroke) {
if (typeof(rad) == "undefined") rad = 5;
this.beginPath();
this.moveTo(xx+rad, yy);
this.arcTo(xx+ww, yy, xx+ww, yy+hh, rad);
this.arcTo(xx+ww, yy+hh, xx, yy+hh, rad);
this.arcTo(xx, yy+hh, xx, yy, rad);
this.arcTo(xx, yy, xx+ww, yy, rad);
if (stroke) this.stroke(); // Default to no stroke
if (fill || typeof(fill)=="undefined") this.fill(); // Default to fill
}; // end of fillRoundedRect method
}
The code checks to see if the prototype for the constructor for the canvas context object contains a 'fillRoundedRect
' property and adds one -- the first time around. It is invoked in the same manner as the fillRect
method:
ctx.fillStyle = "#eef"; ctx.strokeStyle = "#ddf";
// ctx.fillRect(10,10, 200,100);
ctx.fillRoundedRect(10,10, 200,100, 5);
The method uses the arcTo
method as Grumdring did. In the method, this
is a reference to the ctx
object. The stroke argument defaults to false if undefined. The fill argument defaults to fill the rectangle if undefined.
(Tested on Firefox, I don't know if all implementations permit extension in this manner.)
rad = Math.min( rad, ww/2, hh/2 );
so that this works with large radii as in @Grumdrig's version.
try to add this line , when you want to get rounded corners : ctx.lineCap = "round";
Method 1: Using path-drawing methods
The most straightforward method of doing this with HTML Canvas is by using the path-drawing methods of ctx
:
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); function roundedRect(ctx, x, y, width, height, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); } ctx.fillStyle = "red"; roundedRect(ctx, 10, 10, 100, 100, 20); ctx.fill();
Method 2: Using Path2D
You can also draw rounded rectangles in HTML Canvas by using the Path2D
interface:
Example 1
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); function roundedRect(x, y, width, height, radius) { return new Path2D(`M ${x + radius} ${y} H ${x + width - radius} a ${radius} ${radius} 0 0 1 ${radius} ${radius} V ${y + height - radius} a ${radius} ${radius} 0 0 1 ${-radius} ${radius} H ${x + radius} a ${radius} ${radius} 0 0 1 ${-radius} ${-radius} V ${y + radius} a ${radius} ${radius} 0 0 1 ${radius} ${-radius}`); } ctx.fillStyle = "blue"; ctx.fill(roundedRect(10, 10, 100, 100, 20));
Example 2
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); function roundedRect(x, y, width, height, radius) { let path = new Path2D(); path.moveTo(x + radius, y); path.lineTo(x + width - radius, y); path.quadraticCurveTo(x + width, y, x + width, y + radius); path.lineTo(x + width, y + height - radius); path.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); path.lineTo(x + radius, y + height); path.quadraticCurveTo(x, y + height, x, y + height - radius); path.lineTo(x, y + radius); path.quadraticCurveTo(x, y, x + radius, y); path.closePath(); return path; } ctx.fillStyle = "green"; ctx.fill(roundedRect(10, 10, 100, 100, 20));
NONE of the other answers can handle the following 3 cases correctly:
if ((width >= radius x 2) && (height <= radius * 2))
if ((width <= radius x 2) && (height >= radius * 2))
if ((width <= radius x 2) && (height <= radius * 2))
If any of these cases happen, you will not get a correctly drawn rectangle
My Solution handles ANY radius and ANY Width and Height dynamically, and should be the default answer
function roundRect(ctx, x, y, width, height, radius) {
/*
* Draws a rounded rectangle using the current state of the canvas.
*/
let w = width;
let h = height;
let r = radius;
ctx.stroke()
ctx.fill()
ctx.beginPath();
// Configure the roundedness of the rectangles corners
if ((w >= r * 2) && (h >= r * 2)) {
// Handles width and height larger than diameter
// Keep radius fixed
ctx.moveTo(x + r, y); // tr start
ctx.lineTo(x + w - r, y); // tr
ctx.quadraticCurveTo(x + w, y, x + w, y + r); //tr
ctx.lineTo(x + w, y + h - r); // br
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); // br
ctx.lineTo(x + r, y + h); // bl
ctx.quadraticCurveTo(x, y + h, x, y + h - r); // bl
ctx.lineTo(x, y + r); // tl
ctx.quadraticCurveTo(x, y, x + r, y); // tl
} else if ((w < r * 2) && (h > r * 2)) {
// Handles width lower than diameter
// Radius must dynamically change as half of width
r = w / 2;
ctx.moveTo(x + w, y + h - r); // br start
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); // br curve
ctx.quadraticCurveTo(x, y + h, x, y + h - r) // bl curve
ctx.lineTo(x, y + r); // line
ctx.quadraticCurveTo(x, y, x + r, y); // tl
ctx.quadraticCurveTo(x + w, y, x + w, y + r); // tl
ctx.lineTo(x + w, y + h - r); // line
} else if ((w > r * 2) && (h < r * 2)) {
// Handles height lower than diameter
// Radius must dynamically change as half of height
r = h / 2;
ctx.moveTo(x + w - r, y + h); // br start
ctx.quadraticCurveTo(x + w, y + h, x + w, y + r); // br curve
ctx.quadraticCurveTo(x + w, y, x + w - r, y); // tr curve
ctx.lineTo(x + r, y); // line between tr tl
ctx.quadraticCurveTo(x, y, x, y + r); // tl curve
ctx.quadraticCurveTo(x, y + h, x + r, y + h); // bl curve
} else if ((w < 2 * r) && (h < 2 * r)) {
// Handles width and height lower than diameter
ctx.moveTo(x + w / 2, y + h);
ctx.quadraticCurveTo(x + w, y + h, x + w, y + h / 2); // bl curve
ctx.quadraticCurveTo(x + w, y, x + w / 2, y); // tr curve
ctx.quadraticCurveTo(x, y, x, y + h / 2); // tl curve
ctx.quadraticCurveTo(x, y + h, x + w / 2, y + h); // bl curve
}
ctx.closePath();
}
Success story sharing