Как нарисовать прямоугольник с закругленными углами с помощью HTML Canvas?

HTML Canvas предоставляет методы для рисования прямоугольников, fillRect() и strokeRect(), но я не могу найти метод для создания прямоугольников с закругленными углами. Как я могу это сделать?

Свободный перевод вопроса How to draw a rounded rectangle using HTML Canvas? от участника @DNB5brims.


Ответы (5 шт):

Автор решения: Alexandr_TT

Итак, это основано на использовании 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>

Свободный перевод ответа от участника @Woold.

→ Ссылка
Автор решения: Alexandr_TT

roundRect(x, y, width, height, radii); - теперь официально является частью Canvas 2D API.

Он отображается в объектах CanvasRenderingContext2D, Path2D и OffscreenCanvasRenderingContext2D.

Его параметр radii - это массив, содержащий либо:

  • один float, представляющий радиус, используемый для всех четырех углов,
  • два float для верхнего левого + нижнего правого и верхнего правого + нижнего левого углов соответственно,
  • три float для верхнего левого, верхнего правого + нижнего левого и нижнего правого соответственно,
  • или четыре float, по одному на угол,
  • ИЛИ те же комбинации, но с объектом DOMPointInit, представляющим x-радиус и y-радиус каждого угла.

В настоящее время доступна реализация только в Chrome (под флагом, которая по-прежнему не поддерживает объекты DOMPointInit, а только истинные DOMPoints), и вы можете найти созданный мной полифилл

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/roundRect.js"></script>
<canvas></canvas>

Свободный перевод ответа от участника @Kaiido.

→ Ссылка
Автор решения: Alexandr_TT

Мне нужно было сделать то же самое, и я создал для этого метод.

Прочтите пожалуйста комментарии в коде:

// Сейчас можно просто вызвать 
var ctx = document.getElementById("rounded-rect").getContext("2d");
// Рисование с использованием радиуса границы по умолчанию, 
// обводка, но без заливки (значения функции по умолчанию)
roundRect(ctx, 5, 5, 50, 50);
// Чтобы изменить цвет прямоугольника, просто измените контекст
ctx.strokeStyle = "rgb(255, 0, 0)";
ctx.fillStyle = "rgba(255, 255, 0, .5)";
roundRect(ctx, 100, 5, 100, 100, 20, true);
// Снова манипулируйте им
ctx.strokeStyle = "#0f0";
ctx.fillStyle = "#ddd";
// Различные радиусы для каждого угла, другие по умолчанию равны 0
roundRect(ctx, 300, 5, 200, 100, {
  tl: 50,
  br: 25
}, true);

/**
 * Рисует прямоугольник с закругленными углами, используя текущее состояние холста.
 * Если вы опустите последние три параметра, он будет рисовать прямоугольник
 * контур с радиусом границы 5 пикселей
 * @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, fill, stroke) {
  if (typeof stroke === 'undefined') {
    stroke = true;
  }
  if (typeof radius === 'undefined') {
    radius = 5;
  }
  if (typeof radius === 'number') {
    radius = {tl: radius, tr: radius, br: radius, bl: radius};
  } else {
    var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
    for (var side in defaultRadius) {
      radius[side] = radius[side] || defaultRadius[side];
    }
  }
  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();
  }

}
<canvas id="rounded-rect" width="500" height="200">
  <!-- Insert fallback content here -->
</canvas>

Свободный перевод ответа от участника @Juan Mendes.

→ Ссылка
Автор решения: Grundy

Еще вариант: использовать .arcTo позволяющий рисовать конкретную дугу

Если учитывать радиус углов может выйти следующая функция

var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext('2d');

function roundRect(ctx, x1, y1, x2, y2, radius) {
  radius = Math.min(radius, (x2 - x1) / 2, (y2 - y1) / 2); // избегаем артефактов, в случае если радиус скругления больше одной из сторон
  ctx.beginPath();
  ctx.moveTo(x1 + radius, y1);
  ctx.lineTo(x2 - radius, y1);
  ctx.arcTo(x2, y1, x2, y1 + radius, radius);
  ctx.lineTo(x2, y2 - radius);
  ctx.arcTo(x2, y2, x2 - radius, y2, radius);
  ctx.lineTo(x1 + radius, y2);
  ctx.arcTo(x1, y2, x1, y2 - radius, radius);
  ctx.lineTo(x1, y1 + radius);
  ctx.arcTo(x1, y1, x1 + radius, y1, radius);
  ctx.stroke();
}

function yop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#0000ff";
  ctx.strokeStyle = "#ff0000";

  roundRect(ctx, 10, 10, 200, 160, +radius.value);
  requestAnimationFrame(yop);
}


requestAnimationFrame(yop);
<input type="range" min="1" max="75" id="radius">

<canvas id="myCanvas" width="600" height="400">
</canvas>

→ Ссылка
Автор решения: Leonid

Можно генерировать путь этого прямоугольника, а уже к нему применять stroke или fill. Проверяю радиус, чтобы он был не более половины меньшей стороны прямоугольника.

Функцию можно переделать и на разные радиусы, просто заменить r для каждой строки, скажем, на r[0] (верхняя строка для верхнего правого угла) и так далее по часовой стрелке.

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

ctx.stroke(new Path2D(roundedRectPath(10,10,100,80,20)));
ctx.fill(new Path2D(roundedRectPath(150,10,120,60,30)));


function roundedRectPath(x,y,w,h,r){
    r = (Math.min(w,h)/2 > r)? r : Math.min(w,h)/2;
    return `M ${x + r} ${y} l ${w-2*r} 0 q ${r} 0 ${r} ${r}
        l 0 ${h-2*r} q 0 ${r} ${-r} ${r}
        l ${-w+2*r} 0 q ${-r} 0 ${-r} ${-r}
        l 0 ${-h+2*r} q 0 ${-r} ${r} ${-r}`;
}
<canvas></canvas>

Добавил возможность разных радиусов для углов через добавления массива из 1, 2 и 4 значений (если два, то сначала правые, а потом левые). Проверку на превышение половины одной из сторон делаю только для 1 значения.

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 600;
canvas.height = 180;

ctx.stroke(new Path2D(roundedRectPath(10,10,100,80,[10,20,30,40])));
ctx.fill(new Path2D(roundedRectPath(150,10,120,60,[30,10])));

ctx.fillStyle = 'red';
ctx.fill(new Path2D(roundedRectPath(280,10,80,150,[200])));

ctx.strokeStyle = 'purple';
ctx.stroke(new Path2D(roundedRectPath(380,60,150,30,20)));


function roundedRectPath(x,y,w,h,r){
    if(!Array.isArray(r)){
        r = (Math.min(w,h)/2 > r)? r : Math.min(w,h)/2;
        r = [r,r,r,r];
    } else {
        if(r.length == 1){
            r = (Math.min(w,h)/2 > r[0])? r[0] : Math.min(w,h)/2;
            r = [r,r,r,r];
        } else if(r.length == 2){
            r = [r[0], r[0], r[1], r[1]];
        }
    }
    
    return `M ${x + r[3]} ${y} l ${w-r[3]-r[0]} 0 q ${r[0]} 0 ${r[0]} ${r[0]}
        l 0 ${h-r[0]-r[1]} q 0 ${r[1]} ${-r[1]} ${r[1]}
        l ${-w+r[1]+r[2]} 0 q ${-r[2]} 0 ${-r[2]} ${-r[2]}
        l 0 ${-h+r[2]+r[3]} q 0 ${-r[3]} ${r[3]} ${-r[3]}`;
}
<canvas></canvas>

Update

Для объяснения кода добавляю пример с обычным прямым углом и со скругленным, параметры которого можно менять (rx,ry). Тут становится понятна реализация "нативного" метода roundRect(), где последний параметр radii (радиусы) может иметь несколько форматов. Это может быть список из 1, 2, 3 или 4 чисел, а также DOMPointInit (состоящий из радиуса[x] и радиуса[y]).

Затем по некоторому алгоритму входящие данные преобразуются в т.н. список нормализованных радиусов. [r] преобразуется в [r,r,r,r], [r1,r2] => [r1,r2,r1,r2], [r1,r2,r3] => [r1,r2,r2,r3]. И [r1,r2,r3,r4] остается тем же. Отсчет идет от левого верхнего угла и по часовой стрелке. (Хотя в коде выше алгоритм другой).

Соответственно DOMPointInit может быть представлен любой из радиусов. Если бы это был просто массив из двух чисел, то было бы не совсем очевидно как их использовать, ведь по ходу "движения" векторного пути первое значение не всегда по х, поэтому по сути это сложный радиус представлен {x:rx, y:ry}. И по этой же причине arcTo или arc не могут быть использованы в качестве скругления ведь они имеют один радиус. Кроме того, не обеспечивается касательность дуги к продолжению прямой.

Напротив, квадратичная Безье (q, Q в краткой форме SVG путей) при условии, что контрольная точка совпадает с условной точкой угла, всегда обеспечит касательность к оставшимся от этого угла прямым. А разные радиусы rx и ry обеспечиваются соответствующими отступами от условного угла. Это и демонстрирует код ниже.

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const w = canvas.width = 400;
const h = canvas.height = 180;

drawAngles(100,100);

function drawAngles(rx,ry){
    ctx.clearRect(0,0,w,h);
    
    // A simple angle
    ctx.stroke(new Path2D(`M 10 10 l 160 0 0 160`)); // line has relative coords
    // Draw points of the angle
    drawPoint('red', 4, 10, 10); // The starting point
    drawPoint('red', 4, 170, 10); // The corner point
    drawPoint('red', 4, 170, 170); // The end point
    
    let path = '';
    
    path += `M 200 10 `; // The starting point
    drawPoint('red', 4, 200, 10);
    path += `l ${160 - rx} 0 `; // First straight line (relative coords from the starting point)
    drawPoint('blue', 4, 360 - rx, 10);
    path += `q ${rx} 0 `; // The control point of quadratic Bezier (the corner)
    drawPoint('purple', 4, 360, 10);
    path += `${rx} ${ry} `; // The end point of Bezier curve
    drawPoint('blue', 4, 360, 10 + ry);
    path += `l 0 ${160 - ry}`; // The second straigth line (end point of it)
    drawPoint('red', 4, 360, 170);
    
    ctx.stroke(new Path2D(path));   
}

function drawPoint(color,radius,cx,cy){
    ctx.save();
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(cx,cy,radius,0,2*Math.PI,false);
    ctx.fill();
    ctx.restore();
}
<canvas></canvas>
<form oninput="drawAngles(+rx.value,+ry.value)" style="float: right; display:flex; flex-direction: column">
    <input type="range" min="0" max="160" name="rx" value="100">
    <input type="range" min="0" max="160" name="ry" value="100">
</form>

→ Ссылка