Jak narysować zaokrąglony prostokąt na płótnie HTML?


Odpowiedzi:


47

Kanwa HTML5 nie zapewnia metody rysowania prostokąta z zaokrąglonymi narożnikami.

A co z użyciem metod lineTo()i arc()?

Możesz także użyć quadraticCurveTo()metody zamiast arc()metody.


Z jakiegoś powodu wydaje mi się, że mam problemy z arcTo w przeglądarkach Firefox 3.5 i Opera 10.0. Podobna do tej witryny: ditchnet.org/canvas/CanvasRoundedCornerExample.html
bgw

ArcTo został poprawiony w najnowszej wersji FF.
Ash Blue

3
Czy możesz podać przykład?
Jean-Pierre Bécotte

324

Musiałem zrobić to samo i stworzyłem metodę, aby to zrobić.

// 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
Doskonała odpowiedź ... Dlaczego to jeszcze nie jest rodzime dla płótna ?! Dzięki.
andygoestohollywood,

1
kod ma błąd, musi wykonać obrys PO wypełnieniu, w przeciwnym razie na małych prostokątach wypełnienie nadpisze obrys.
Zig Mandel

2
Nie mam pod ręką przykładu, ale musiałem zmodyfikować tę kolejność dla przypadku, który testowałem na moim kodzie. To logiczne, jak może poprawnie obrysować (z wygładzaniem za pomocą prostego koloru tła), jeśli jeszcze nie wypełniłeś prostego?
Zig Mandel

2
@Juan hej, moja wina, zauważyłem post na blogu i złapałem tę ciekawostkę później. Chciałem cofnąć zmianę. Goodjob man dał ci +1! 😁
fabbb

6
Zig Mandel ma rację: należy go napełnić, a następnie głaskać. Powodem jest to, że jeśli obrysujesz, a następnie wypełnisz, szerokość linii zmniejszy się o połowę. Spróbuj z bardzo grubą linią (powiedzmy 20) i porównaj zaokrąglony prostokąt, który jest wypełniony kolorem tła, z zaokrąglonym prostokątem, który nie jest wypełniony. Szerokość linii wypełnionej będzie równa połowie szerokości linii niewypełnionej.
Andrew Stacey,

106

Zacząłem od rozwiązania @ jhoff, ale przepisałem je tak, aby używał parametrów szerokości / wysokości, a użycie arcTosprawia, że ​​jest trochę bardziej zwięzły:

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

Zwróć także kontekst, abyś mógł trochę połączyć. Na przykład:

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

4
Nie zadzierałbym z kontekstem renderowania Canvas, z wyjątkiem tego dobrego rozwiązania.
Ash Blue

Problem z tym rozwiązaniem polega na tym, że nie można niezależnie kontrolować promienia dla każdego narożnika. Niewystarczająco elastyczny. Zobacz moje rozwiązanie poniżej.
Corgalore

1
To jest wyśrodkowany prostokąt, jeśli ktoś potrzebuje go z lewym górnym rogiem w środku (x,y), zapisz kontekst, dodaj tłumaczenie (-w/2,-h/2)i przywróć kontekst.
nessa.gp

Dziękuję, jest to jedyny, który jak dotąd działał dla mnie, inne dają mi problemy, gdy promień był większy lub większy niż wysokość lub szerokość. Wdrożone!
Howzieky

1
Zauważ, że to rozwiązanie działa, aby każdy wielokąt miał zaokrąglone rogi. Skrzypce .
Doguleez

23

Juan, nieznacznie ulepszyłem twoją metodę, aby umożliwić indywidualną zmianę promienia każdego prostokąta:

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

Użyj tego w ten sposób:

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
Takie podejście zapewnia dużą kontrolę nad zaokrąglonymi narożnikami. Dlaczego nie jest to akceptowana odpowiedź>
Vighnesh Raut

@VighneshRaut Prawdopodobnie dlatego, że ta odpowiedź skopiowała / wkleiła oryginalną zaakceptowaną odpowiedź i dodała zaokrąglone rogi. Włączyłem to do zaakceptowanej odpowiedzi i przypisałem tę odpowiedź. Zaakceptowana odpowiedź ma przykład na żywo, a składnia jest prostsza, jeśli chcesz, aby wszystkie narożniki miały ten sam promień (co jest najczęstszym przypadkiem). Wreszcie ta odpowiedź sugeruje modyfikację prototypu rodzimego obiektu, który jest nie-nie
Juan Mendes

12

Poniższa drawPolygonfunkcja może służyć do rysowania dowolnych plików wielokąta z zaokrąglonymi narożnikami.

Zobacz, jak działa tutaj.

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

Funkcja otrzymuje tablicę z punktami wielokątów, na przykład:

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

To jest port i bardziej ogólna wersja rozwiązania zamieszczonego tutaj .


9

Oto jeden, który napisałem ... używa łuków zamiast krzywych kwadratowych dla lepszej kontroli promienia. Pozostawia ci głaskanie i wypełnienie

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

Oto przykład:

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

W jaki sposób daje to lepszą kontrolę nad promieniem? Myślałem, że pozwolisz na promienie x / y (owalne rogi), a także określisz różne promienie dla każdego narożnika
Juan Mendes

3
Twój r2dprawdopodobnie chce być wezwany d2r.
Grumdrig

1
@JuanMendes: (oparte na łukach) kształty zaokrąglonych rogów w tym rozwiązaniu są bardziej okrągłe niż w rozwiązaniu (opartym na kwadratach). Myślę, że właśnie to miał na myśli, mówiąc „lepszą kontrolę nad promieniem”.
Brent Bradburn

Ja też użyłem tej metody, jest lepsza niż użycie quadraticCurve. Ale jeśli narysujesz coś bardziej złożonego niż prostokąt, jest to NAPRAWDĘ bolesne. Była tam automatyczna metoda, taka jak w kanwie Androida.
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();

właśnie tego szukałem
Daniel

Na koniec krótka i wyczerpująca odpowiedź, która faktycznie działa. dzięki.
Franz Skuffka

5

Jest to więc oparte na użyciu lineJoin = "round" i przy odpowiednich proporcjach, matematyce i logice udało mi się wykonać tę funkcję, nie jest to idealne, ale mam nadzieję, że pomoże. Jeśli chcesz, aby każdy narożnik miał inny promień, spójrz na: https://p5js.org/reference/#/p5/rect

Proszę bardzo:

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
Witamy w StackOverflow! Ponieważ ten kod może rozwiązać problem, lepiej, jeśli dodasz więcej wyjaśnień, jak to działa.
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();
    };
}

Ponieważ Opera przechodzi na WebKit, powinno to również obowiązywać w starszym przypadku.


3

Aby funkcja była bardziej spójna ze zwykłymi sposobami korzystania z kontekstu kanwy, klasę kontekstu kanwy można rozszerzyć o fillRoundedRectmetodę „ ” - którą można wywołać w ten sam sposób 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
} 

Kod sprawdza, czy prototyp konstruktora obiektu kontekstu kanwy zawiera fillRoundedRectwłaściwość „ ” i dodaje ją - za pierwszym razem. Jest wywoływana w taki sam sposób jak fillRectmetoda:

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

Metoda wykorzystuje arcTometodę, tak jak zrobił to Grumdring. W metodzie thisjest odniesieniem do ctxobiektu. Argument stroke przyjmuje wartość domyślną false, jeśli jest niezdefiniowany. Argument wypełnienia domyślnie wypełnia prostokąt, jeśli jest niezdefiniowany.

(Testowane w przeglądarce Firefox, nie wiem, czy wszystkie implementacje pozwalają na rozszerzenie w ten sposób).


1
Proponuję dodać: rad = Math.min( rad, ww/2, hh/2 );żeby to działało z dużymi promieniami jak w wersji @ Grumdrig.
Brent Bradburn

3

Oto rozwiązanie wykorzystujące lineJoin do zaokrąglania rogów. Działa, jeśli potrzebujesz tylko stałego kształtu, ale nie tak bardzo, jeśli potrzebujesz cienkiej ramki, która jest mniejsza niż promień obramowania.

    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

spróbuj dodać tę linię, jeśli chcesz uzyskać zaokrąglone rogi: ctx.lineCap = "round";


1
Cześć, witamy w przepełnieniu stosu. Zajrzyj tutaj . Czy na pewno jest to użyteczna odpowiedź dla prostokątów?
Jeroen Heier
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.