Comment dessiner un rectangle arrondi sur HTML Canvas?

Réponses:

47

Le canevas HTML5 ne fournit pas de méthode pour dessiner un rectangle avec des coins arrondis.

Que diriez-vous d'utiliser les méthodes lineTo()et arc()?

Vous pouvez également utiliser la quadraticCurveTo()méthode au lieu de la arc()méthode.

Codeur-256
la source
Pour une raison quelconque, je semble avoir des problèmes avec arcTo dans Firefox 3.5 et Opera 10.0. Similaire à ce site: ditchnet.org/canvas/CanvasRoundedCornerExample.html
bgw
arcTo a été corrigé dans la dernière version de FF.
Ash Blue
3
Pouvez vous donner un exemple?
Jean-Pierre Bécotte
324

J'avais besoin de faire la même chose et j'ai créé une méthode pour le faire.

// 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
la source
2
Réponse parfaite ... Comment n'est-ce pas encore natif de la toile?! Merci.
andygoestohollywood
1
le code a un bug, il doit faire le trait APRÈS le remplissage, sinon sur les petits rectangles le remplissage écrasera le trait.
Zig Mandel
2
Je n'ai pas l'exemple sous la main mais j'ai dû modifier cet ordre pour un cas que j'ai testé sur mon code. C'est logique, comment peut-il correctement tracer (avec lissage en utilisant la couleur d'arrière-plan rect) si vous n'avez pas encore rempli le rect?
Zig Mandel du
2
@Juan hey my bad, j'ai remarqué le billet de blog et j'ai attrapé cette friandise après. Je voulais annuler la modification. Goodjob man + 1'd you! 😁
fabbb
6
Zig Mandel a raison: il faut le remplir puis le caresser. La raison en est que si vous tracez puis remplissez, la largeur de la ligne est réduite de moitié. Essayez-le avec une largeur de ligne très épaisse (disons 20) et comparez un rectangle arrondi rempli de la couleur d'arrière-plan à un rectangle arrondi qui n'est pas rempli. La largeur de ligne de celle remplie correspondra à la moitié de la largeur de ligne de celle non remplie.
Andrew Stacey
106

J'ai commencé avec la solution de @ jhoff, mais je l'ai réécrite pour utiliser les paramètres largeur / hauteur, et l'utilisation la arcTorend un peu plus laconique:

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

Renvoie également le contexte pour pouvoir enchaîner un peu. Par exemple:

ctx.roundRect(35, 10, 225, 110, 20).stroke(); //or .fill() for a filled rect
Grumdrig
la source
4
Je ne voudrais pas jouer avec le contexte de rendu de Canvas, sauf pour cette bonne solution.
Ash Blue
Le problème avec cette solution est que vous ne pouvez pas contrôler le rayon de chaque coin indépendamment. Pas assez flexible. Voir ma solution ci-dessous.
Corgalore
1
Il s'agit d'un rectangle centré, si quelqu'un en a besoin avec son coin supérieur gauche (x,y), enregistrez le contexte, ajoutez une traduction (-w/2,-h/2)et restaurez le contexte.
nessa.gp
Merci, c'est le seul qui a fonctionné pour moi jusqu'à présent, les autres me donnant des problèmes lorsque le rayon était supérieur ou supérieur à la hauteur ou à la largeur. Mis en œuvre!
Howzieky
1
Notez que cette solution fonctionne pour que tout polygone ait des coins arrondis. Un violon .
Doguleez
23

Juan, j'ai apporté une légère amélioration à votre méthode pour permettre de changer le rayon de chaque coin de rectangle individuellement:

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

Utilisez-le comme ceci:

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
la source
1
Cette approche offre tellement de contrôle sur les coins arrondis. Pourquoi n'est-ce pas la réponse acceptée>
Vighnesh Raut
@VighneshRaut Probablement parce que cette réponse a copié / collé la réponse originale acceptée et ajouté des coins arrondis. Je l'ai incorporé dans la réponse acceptée a donné crédit à cette réponse. La réponse acceptée a un exemple en direct et la syntaxe est plus simple si vous voulez que tous les coins aient le même rayon (ce qui est le cas le plus courant). Enfin, cette réponse suggère de modifier le prototype d'un objet natif qui est un non-non
Juan Mendes
12

La drawPolygonfonction ci-dessous peut être utilisée pour dessiner n'importe quel polygone avec des coins arrondis.

Regardez-le en cours d'exécution ici.

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 fonction reçoit un tableau avec les points du polygone, comme ceci:

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

Ceci est un port et une version plus générique d'une solution publiée ici .

moraes
la source
9

En voici un que j'ai écrit ... utilise des arcs au lieu de courbes quadratiques pour un meilleur contrôle du rayon. En outre, il vous laisse le soin et le remplissage

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

Voici un exemple:

var _e = document.getElementById('#my_canvas');
var _cxt = _e.getContext("2d");
_cxt.roundRect(35,10,260,120,20);
_cxt.strokeStyle = "#000";
_cxt.stroke();
jhoff
la source
Comment cela vous donne-t-il un meilleur contrôle sur le rayon? Je pensais que vous alliez autoriser les rayons x / y (coins ovales), et également spécifier différents rayons pour chaque coin
Juan Mendes
3
Vous voulez r2dprobablement être appelé d2r.
Grumdrig
1
@JuanMendes: Les formes (à base d'arc) des coins arrondis de cette solution sont plus circulaires que celles de votre solution (à base quadratique). Je pense que c'est ce qu'il entendait par «meilleur contrôle du rayon».
Brent Bradburn
J'ai également utilisé cette méthode, c'est mieux que d'utiliser quadraticCurve. Mais si vous dessinez quelque chose de plus complexe qu'un rectangle, c'est VRAIMENT douloureux. Avec il y avait une méthode automatique comme dans le canevas 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();
atome
la source
c'était exactement ce que je cherchais
Daniel
Enfin une réponse brève et complète qui fonctionne réellement. Merci.
Franz Skuffka
5

Donc, ceci est basé sur l'utilisation de lineJoin = "round" et avec les bonnes proportions, les mathématiques et la logique, j'ai pu faire cette fonction, ce n'est pas parfait mais j'espère que cela aide. Si vous voulez que chaque coin ait un rayon différent, jetez un œil à: https://p5js.org/reference/#/p5/rect

Et voilà:

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
la source
1
Bienvenue dans StackOverflow! Étant donné que ce code peut résoudre le problème, il est préférable d'ajouter plus d'explications sur son fonctionnement.
Tân le
3

Opéra, 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();
    };
}

Depuis Opera va WebKit, cela devrait également rester valable dans le cas hérité.

dcode
la source
3

Pour rendre la fonction plus cohérente avec les moyens normaux d'utilisation d'un contexte de canevas, la classe de contexte canevas peut être étendue pour inclure une fillRoundedRectméthode ' ' - qui peut être appelée de la même manière fillRectest appelée:

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
} 

Le code vérifie si le prototype du constructeur de l'objet de contexte de canevas contient une fillRoundedRectpropriété « » et en ajoute une - la première fois. Elle est invoquée de la même manière que la fillRectméthode:

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

La méthode utilise la arcTométhode comme Grumdring l'a fait. Dans la méthode, thisest une référence à l' ctxobjet. L'argument stroke prend la valeur par défaut false s'il n'est pas défini. L'argument fill par défaut remplit le rectangle s'il n'est pas défini.

(Testé sur Firefox, je ne sais pas si toutes les implémentations permettent l'extension de cette manière.)

Ribo
la source
1
Je suggère d'ajouter: rad = Math.min( rad, ww/2, hh/2 );pour que cela fonctionne avec de grands rayons comme dans la version de @ Grumdrig.
Brent Bradburn
3

Voici une solution utilisant une lineJoin pour arrondir les coins. Fonctionne si vous avez juste besoin d'une forme solide, mais pas tellement si vous avez besoin d'une bordure fine plus petite que le rayon de la bordure.

    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
la source
0

essayez d'ajouter cette ligne, lorsque vous voulez obtenir des coins arrondis: ctx.lineCap = "round";

Olexiy Marchenko
la source
1
Bonjour, bienvenue dans le débordement de pile. Jetez un oeil ici . Êtes-vous sûr que c'est une réponse utilisable pour les rectangles?
Jeroen Heier