Come disegnare un rettangolo arrotondato su tela HTML?


Risposte:


47

La tela HTML5 non fornisce un metodo per disegnare un rettangolo con angoli arrotondati.

Che ne dici di usare i metodi lineTo()e arc()?

È inoltre possibile utilizzare il quadraticCurveTo()metodo anziché il arc()metodo.


Per qualche motivo, mi sembra di avere problemi con arcTo in Firefox 3.5 e Opera 10.0. Simile a questo sito: ditchnet.org/canvas/CanvasRoundedCornerExample.html
bgw

arcTo è stato risolto nell'ultima versione di FF.
Ash Blue,

3
Puoi fornire un esempio?
Jean-Pierre Bécotte,

324

Avevo bisogno di fare la stessa cosa e ho creato un metodo per farlo.

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


2
Risposta perfetta ... Come mai questo non è originario della tela ?! Grazie.
andygoestohollywood,

1
il codice ha un bug, deve eseguire il tratto DOPO il riempimento, altrimenti su piccoli rettangoli il riempimento sovrascriverà il tratto.
Zig Mandel,

2
Non ho l'esempio a portata di mano, ma ho dovuto modificare quell'ordine per un caso che ho testato sul mio codice. È logico, come può accarezzare correttamente (con levigatura usando il colore di sfondo retto) se non hai ancora riempito il retto?
Zig Mandel,

2
@Juan hey mio male, ho notato il post sul blog e ho preso quel bocconcino dopo. Volevo annullare la modifica. Goodjob man + 1 you you! 😁
fabbb

6
Zig Mandel è corretto: dovrebbe essere riempito e poi accarezzato. Il motivo è che se si traccia e poi si riempie, la larghezza della linea viene dimezzata. Provalo con una larghezza della linea molto spessa (diciamo, 20) e confronta un rettangolo arrotondato che è riempito con il colore di sfondo con un rettangolo arrotondato che non è riempito. La larghezza della linea di quella riempita sarà la metà della larghezza della linea di quella non riempita.
Andrew Stacey,

106

Ho iniziato con la soluzione di @ jhoff, ma l'ho riscritto per utilizzare i parametri larghezza / altezza e l'utilizzo lo arcTorende un po 'più 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;
}

Restituisce anche il contesto in modo da poter concatenare un po '. Per esempio:

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

4
Non farei confusione con il contesto di rendering di Canvas, ad eccezione di quella buona soluzione.
Ash Blue,

Il problema con questa soluzione è che non puoi controllare il raggio per ogni angolo in modo indipendente. Non abbastanza flessibile. Vedi la mia soluzione di seguito.
Corgalore,

1
Questo è un rettangolo centrato, se qualcuno ne ha bisogno con uno con l'angolo in alto a sinistra (x,y), salva il contesto, aggiungi una traduzione (-w/2,-h/2)e ripristina il contesto.
nessa.gp,

Grazie, questo è l'unico che ha funzionato finora per me, gli altri mi hanno dato problemi quando il raggio era maggiore o maggiore dell'altezza o della larghezza. Implementato!
Howzieky,

1
Nota che questa soluzione funziona per rendere qualsiasi poligono con angoli arrotondati. Un violino .
Doguleez,

23

Juan, ho apportato un leggero miglioramento al tuo metodo per consentire di modificare singolarmente ogni raggio dell'angolo del rettangolo:

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

Usalo in questo modo:

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

1
Questo approccio fornisce così tanto controllo sugli angoli arrotondati. Perché questa non è la risposta accettata>
Vighnesh Raut,

@VighneshRaut Probabilmente perché questa risposta ha copiato / incollato la risposta originale accettata e aggiunto angoli arrotondati. L'ho incorporato nella risposta accettata per dare credito a questa risposta. La risposta accettata ha un esempio reale e la sintassi è più semplice se si desidera che tutti gli angoli abbiano lo stesso raggio (che è il caso più comune). Infine, questa risposta suggerisce di modificare il prototipo di un oggetto nativo che è un no-no
Juan Mendes,

12

La drawPolygonseguente funzione può essere utilizzata per disegnare qualsiasi poligono con angoli arrotondati.

Guardalo correre qui.

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 funzione riceve un array con i punti poligonali, in questo modo:

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

Questa è una porta e una versione più generica di una soluzione pubblicata qui .


9

Eccone uno che ho scritto ... usa archi invece di curve quadratiche per un migliore controllo del raggio. Inoltre, lascia la carezza e il riempimento a te

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

Ecco un esempio:

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

In che modo questo ti dà un migliore controllo sul raggio? Pensavo che avresti consentito i raggi x / y (angoli ovali) e specificando anche raggi diversi per ogni angolo
Juan Mendes,

3
Il tuo r2dprobabilmente vuole essere chiamato d2r.
Grumdrig,

1
@JuanMendes: le forme (basate sull'arco) degli angoli arrotondati in questa soluzione sono più circolari di quelle della tua soluzione (basata sul quadratico). Penso che questo sia ciò che intendeva per "un migliore controllo del raggio".
Brent Bradburn,

Ho anche usato questo metodo, è meglio che usare quadraticCurve. Ma se disegni qualcosa di più complesso del rettangolo, è DAVVERO doloroso. Con c'era un metodo automatico come nella tela 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();

questo era esattamente quello che stavo cercando
Daniel,

Finalmente una risposta breve e completa che funziona davvero. Grazie.
Franz Skuffka,

5

Quindi questo si basa sull'utilizzo di lineJoin = "round" e con le proporzioni, la matematica e la logica appropriate sono stato in grado di fare questa funzione, questo non è perfetto, ma spero che aiuti. Se vuoi fare in modo che ogni angolo abbia un raggio diverso, dai un'occhiata a: https://p5js.org/reference/#/p5/rect

Ecco qui:

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
Benvenuto in StackOverflow! Poiché questo codice può risolvere il problema, è meglio aggiungere ulteriori spiegazioni su come funziona.
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();
    };
}

Poiché Opera sta usando WebKit, questo dovrebbe rimanere valido anche nel caso legacy.


3

Per rendere la funzione più coerente con i normali mezzi di utilizzo di un contesto di tela, la classe di contesto di tela può essere estesa per includere un fillRoundedRectmetodo ' ' - che può essere chiamato allo stesso modo 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
} 

Il codice controlla se il prototipo del costruttore per l'oggetto di contesto canvas contiene una fillRoundedRectproprietà ' ' e ne aggiunge una - la prima volta. Viene invocato allo stesso modo del fillRectmetodo:

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

Il metodo usa il arcTometodo come ha fatto Grumdring. Nel metodo, thisè un riferimento ctxall'oggetto. L'argomento tratto per impostazione predefinita è falso se non definito. L'argomento fill per impostazione predefinita riempie il rettangolo se non definito.

(Testato su Firefox, non so se tutte le implementazioni consentano l'estensione in questo modo.)


1
Suggerisco di aggiungere: in rad = Math.min( rad, ww/2, hh/2 );modo che funzioni con grandi raggi come nella versione di @ Grumdrig.
Brent Bradburn,

3

Ecco una soluzione che utilizza una lineaJoin per arrotondare gli angoli. Funziona se hai solo bisogno di una forma solida ma non tanto se hai bisogno di un bordo sottile più piccolo del raggio del bordo.

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

0

prova ad aggiungere questa linea, quando vuoi ottenere gli angoli arrotondati: ctx.lineCap = "round";


1
Ciao, benvenuto nello stack di overflow. Dai un'occhiata qui . Sei sicuro che questa sia una risposta utilizzabile per i rettangoli?
Jeroen Heier,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.