¿Cómo dibujar un rectángulo redondeado en lienzo HTML?

Respuestas:

47

El lienzo HTML5 no proporciona un método para dibujar un rectángulo con esquinas redondeadas.

¿Qué hay de usar los métodos lineTo()y arc()?

También puede usar el quadraticCurveTo()método en lugar del arc()método.

Codificador-256
fuente
Por alguna razón, parece que tengo problemas con arcTo en Firefox 3.5 y Opera 10.0. Similar a este sitio: ditchnet.org/canvas/CanvasRoundedCornerExample.html
bgw
arcTo se ha corregido en la última versión de FF.
Ash Blue
3
¿Puede dar un ejemplo?
Jean-Pierre Bécotte
324

Necesitaba hacer lo mismo y creé un método para hacerlo.

// 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);

/**
 * 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, 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
fuente
2
Respuesta perfecta ... ¿Cómo es que esto aún no es nativo del lienzo? Gracias.
andygoestohollywood
1
el código tiene un error, debe hacer un trazo DESPUÉS del relleno, de lo contrario, en rectángulos pequeños, el relleno sobrescribirá el trazo.
Zig Mandel
2
No tengo el ejemplo a la mano, pero tuve que modificar ese pedido para un caso que probé en mi código. Es lógico, ¿cómo puede trazar correctamente (con suavizado utilizando el color de fondo rect) si aún no ha llenado el rect?
Zig Mandel
2
@Juan hey mi mal, me di cuenta de la publicación del blog y capté ese dato después. Tenía la intención de deshacer la edición. Goodjob man + 1'd you! Fa
fabbb
66
Zig Mandel es correcto: debe rellenarse y luego acariciarse. La razón es que si trazas y luego llenas, el ancho de la línea se reduce a la mitad. Pruébelo con un ancho de línea realmente grueso (digamos, 20) y compare un rectángulo redondeado que se rellena con el color de fondo con un rectángulo redondeado que no se rellena. El ancho de línea del relleno será la mitad del ancho de línea del relleno.
Andrew Stacey
106

Comencé con la solución de @ jhoff, pero la reescribí para usar parámetros de ancho / alto, y el uso lo arcTohace un poco más conciso:

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;
}

También devuelve el contexto para que pueda encadenar un poco. P.ej:

ctx.roundRect(35, 10, 225, 110, 20).stroke(); //or .fill() for a filled rect
Grumdrig
fuente
44
No me metería con el contexto de representación de Canvas, excepto por esa buena solución.
Ash Blue
El problema con esta solución es que no puede controlar el radio de cada esquina de forma independiente. No lo suficientemente flexible. Vea mi solución a continuación.
Corgalore
1
Este es un rectángulo centrado, si alguien necesita uno con su esquina superior izquierda (x,y), guarde el contexto, agregue una traducción (-w/2,-h/2)y restaure el contexto.
nessa.gp
Gracias, este es el único que me ha funcionado hasta ahora, los otros me dieron problemas cuando el radio era mayor o mayor que la altura o el ancho. ¡Implementado!
Howzieky
1
Tenga en cuenta que esta solución funciona para que cualquier polígono tenga esquinas redondeadas. Un violín .
Doguleez
23

Juan, hice una ligera mejora en tu método para permitir cambiar cada radio de esquina de rectángulo individualmente:

/** 
 * 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();
    }
} 

Úselo así:

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);
Corgalore
fuente
1
Este enfoque proporciona mucho control sobre las esquinas redondeadas. ¿Por qué no es esta la respuesta aceptada>
Vighnesh Raut
@VighneshRaut Probablemente porque esta respuesta copió / pegó la respuesta original aceptada y agregó esquinas redondeadas. Lo incorporé a la respuesta aceptada y le di crédito a esta respuesta. La respuesta aceptada tiene un ejemplo en vivo y la sintaxis es más simple si desea todas las esquinas con el mismo radio (que es el caso más común). Por último, esta respuesta sugiere modificar el prototipo de un objeto nativo que es un no-no
Juan Mendes
12

La drawPolygonsiguiente función se puede usar para dibujar cualquier polígono con esquinas redondeadas.

Véalo corriendo aquí.

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))];
};

La función recibe una matriz con los puntos de polígono, así:

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();

Este es un puerto y una versión más genérica de una solución publicada aquí .

moraes
fuente
9

Aquí hay uno que escribí ... usa arcos en lugar de curvas cuadráticas para un mejor control sobre el radio. Además, te deja acariciando y llenándote

    /* Canvas 2d context - roundRect
 *
 * Accepts 5 parameters, the start_x and start_y points, the end_x and end_y points, and 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();
}

Aquí hay un ejemplo:

var _e = document.getElementById('#my_canvas');
var _cxt = _e.getContext("2d");
_cxt.roundRect(35,10,260,120,20);
_cxt.strokeStyle = "#000";
_cxt.stroke();
jhoff
fuente
¿Cómo te da esto un mejor control sobre el radio? Pensé que ibas a permitir radios x / y (esquinas ovales), y también especificando diferentes radios para cada esquina
Juan Mendes
3
Su r2dprobablemente quiere ser llamado d2r.
Grumdrig
1
@JuanMendes: Las formas (basadas en arco) de las esquinas redondeadas en esta solución son más circulares que las de su solución (basada en cuadrática). Creo que a eso se refería con "mejor control sobre el radio".
Brent Bradburn
También utilicé este método, es mejor que usar quadraticCurve. Pero si dibujas algo más complejo que el rectángulo, es REALMENTE doloroso. Con había un método automático como en el lienzo de Android.
Aleksei Petrenko
7
    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();
átomo
fuente
esto era exactamente lo que estaba buscando
Daniel
Finalmente una respuesta breve y completa que realmente funciona. Gracias.
Franz Skuffka
5

Entonces, esto se basa en el uso de lineJoin = "round" y con las proporciones, matemáticas y lógica adecuadas que he podido hacer esta función, esto no es perfecto, pero espero que ayude. Si desea que cada esquina tenga un radio diferente, eche un vistazo a: https://p5js.org/reference/#/p5/rect

Aqui tienes:

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
fuente
1
¡Bienvenido a StackOverflow! Dado que este código puede resolver el problema, es mejor si agrega más explicaciones sobre cómo funciona.
Tân
3

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();
    };
}

Como Opera se va a WebKit, esto también debería seguir siendo válido en el caso heredado.

dcode
fuente
3

Para que la función sea más coherente con los medios normales de usar un contexto de lienzo, la clase de contexto de lienzo se puede ampliar para incluir un fillRoundedRectmétodo ' ', que se puede llamar de la misma manera fillRect:

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
} 

El código verifica si el prototipo del constructor para el objeto de contexto de lienzo contiene una fillRoundedRectpropiedad ' ' y agrega una, la primera vez. Se invoca de la misma manera que el fillRectmétodo:

  ctx.fillStyle = "#eef";  ctx.strokeStyle = "#ddf";
  // ctx.fillRect(10,10, 200,100);
  ctx.fillRoundedRect(10,10, 200,100, 5);

El método usa el arcTométodo como lo hizo Grumdring. En el método, thises una referencia a lactx objeto. El argumento de trazo por defecto es falso si no está definido. El argumento de relleno por defecto llena el rectángulo si no está definido.

(Probado en Firefox, no sé si todas las implementaciones permiten la extensión de esta manera).

Ribo
fuente
1
Sugiero agregar: rad = Math.min( rad, ww/2, hh/2 );para que esto funcione con radios grandes como en la versión de @ Grumdrig.
Brent Bradburn
3

Aquí hay una solución usando un lineJoin para redondear las esquinas. Funciona si solo necesita una forma sólida, pero no tanto si necesita un borde delgado que sea más pequeño que el radio del borde.

    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];
    let ctx = canvas.getContext('2d');

    roundedRect(ctx, {
        x: 10,
        y: 10,
        width: 200,
        height: 100,
        radius: 10,
        color: "red"
    });
jwerre
fuente
0

intente agregar esta línea cuando desee obtener esquinas redondeadas: ctx.lineCap = "round";

Olexiy Marchenko
fuente
1
Hola, bienvenido a desbordar la pila. Echa un vistazo aquí . ¿Estás seguro de que esta es una respuesta utilizable para rectángulos?
Jeroen Heier