0

I spotted this piece of code that generates a rectangle with rounded corners but I would like to be able to increase the size (height and width) of the rectangle as I want.

 var canvas = document.getElementById('newCanvas');
 var ctx = canvas.getContext('2d');
 ctx.beginPath();
 ctx.moveTo(20, 10);
 ctx.lineTo(80, 10);
 ctx.quadraticCurveTo(90, 10, 90, 20);
 ctx.lineTo(90, 80);
 ctx.quadraticCurveTo(90, 90, 80, 90);
 ctx.lineTo(20, 90);
 ctx.quadraticCurveTo(10, 90, 10, 80);
 ctx.lineTo(10, 20);
 ctx.quadraticCurveTo(10, 10, 20, 10);
 ctx.stroke();
General Grievance
  • 4,555
  • 31
  • 31
  • 45
DoctorPok
  • 86
  • 1
  • 11

2 Answers2

1

You need to convert your static values to (x, y) coordinates and [width × height] dimension variables.

I took what you had and reverse-engineered the formulas to calculate your static drawing. Take your existing variables and change them to x or y and add the width or height to them and optionally add or subtract the radius where necessary.

const drawRoundedRect = (ctx, x, y, width, height, radius) => {
  ctx.beginPath();
  ctx.moveTo(x + radius, y);
  ctx.lineTo(x + width - radius, y);
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
  ctx.lineTo(x + width, y + height - radius);
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
  ctx.lineTo(x + radius, y + height);
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
  ctx.lineTo(x, y + radius);
  ctx.quadraticCurveTo(x, y, x + radius, y);
  ctx.stroke();
};

const canvas = document.getElementById('new-canvas');
const ctx = canvas.getContext('2d');

ctx.strokeStyle = 'black';
ctx.strokeRect(10, 10, 80, 80);

ctx.strokeStyle = 'red';
drawRoundedRect(ctx, 10, 10, 80, 80, 10);

ctx.strokeStyle = 'green';
drawRoundedRect(ctx, 20, 20, 60, 60, 14);
<canvas id="new-canvas"></canvas>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
1

Don't forget to join path ends

I noticed that you forgot to close the path. This can result in a slight seam or bump at the start / end of the path depending on the ctx.lineJoin setting.

The call to ctx.closePath connects the end to the start with a line

Visual design

Visual design rules for the type of curves to use.

  • Beziers for curves that are part of things that move quickly
  • Circle for things that are static or move slowly

Bezier curves can never exactly fit a circle. Quadratic beziers are very bad fits. If you must use a bezier curve use a cubic bezier to get a better fit.

Best approximation of a circle using a cubic bezier is to inset control points by c = 0.55191502449 as fraction of radius. This will result in the minimum possible radial error of 0.019608%

Example shows the difference between a cubic (black) and quadratic (red) curves.

const ctx = canvas.getContext('2d');
ctx.strokeStyle = 'red';
drawRoundedRectQuad(ctx, 10, 10, 180, 180, 70);
ctx.strokeStyle = 'black';
drawRoundedRect(ctx, 10, 10, 180, 180, 70);

function drawRoundedRect(ctx, x, y, w, h, r) {
    const c = 0.55191502449;
    const cP = r * (1 - c);
    const right = x + w;
    const bottom = y + h;
    ctx.beginPath();
    ctx.lineTo(right - r, y);
    ctx.bezierCurveTo(right - cP, y, right, y + cP, right, y + r);
    ctx.lineTo(right, bottom - r);
    ctx.bezierCurveTo(right, bottom - cP, right - cP, bottom, right - r, bottom);
    ctx.lineTo(x + r, bottom);
    ctx.bezierCurveTo(x + cP, bottom, x, bottom - cP, x,  bottom - r);
    ctx.lineTo(x, y + r);
    ctx.bezierCurveTo(x, y + cP , x + cP, y, x + r, y);
    ctx.closePath();
    ctx.stroke();
}
function drawRoundedRectQuad(ctx, x, y, w, h, r){
    ctx.beginPath();
    ctx.lineTo(x + w- r, y);
    ctx.quadraticCurveTo(x + w, y, x + w, y + r);
    ctx.lineTo(x + w, y + h- r);
    ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
    ctx.lineTo(x + r, y + h);
    ctx.quadraticCurveTo(x, y + h, x, y + h- r);
    ctx.lineTo(x, y + r);
    ctx.quadraticCurveTo(x, y, x + r, y);
    ctx.closePath();
    ctx.stroke();
};
<canvas id="canvas" width ="200" height="200"></canvas>

Rounded corners to match CSS border-radius

To get a true rounded rectangle (circles rather than approx curve) use ctx.arc to create the rounded corners.

Extending the 2D API with roundedRect

The code below draws a rounded rectangle by adding the functions strokeRoundedRect(x, y, w, [h, [r]]), fillRoundedRect(x, y, w, [h, [r]]), and roundedRect(x, y, w, [h, [r]]) to the 2D context prototype.

Arguments

x, y, w, [h, [r]]

  • x, y Top left of rounded rectangle
  • w, Width of rounded rectangle
  • h, Optional height of rectangle. Defaults to value of width (creates rounded square)
  • r Optional radius or corners. Default is 0 (no rounded corners). If value is negative then a radius of 0 is used. If r > than half the width or height then r is change to Math.min(w * 0.5, h * 0.5)

Example

Including implementation of round rectangle extensions.

function Extend2DRoundedRect() {
    const p90  = Math.PI * 0.5;
    const p180 = Math.PI;
    const p270 = Math.PI * 1.5;
    const p360 = Math.PI * 2;
    function roundedRect(x, y, w, h = w, r = 0) {
        const ctx = this;
        if (r < 0) { r = 0 }
        if (r === 0) {
            ctx.rect(x, y, w, h);
            return;
        }
        r = Math.min(r, w * 0.5, h * 0.5)
        ctx.moveTo(x, y + r);   
        ctx.arc(x + r    , y + r    , r, p180, p270);
        ctx.arc(x + w - r, y + r    , r, p270, p360);
        ctx.arc(x + w - r, y + h - r, r, 0   , p90);
        ctx.arc(x + r    , y + h - r, r, p90 , p180);
        ctx.closePath();
    }
    function strokeRoundedRect(...args) {
        const ctx = this;
        ctx.beginPath();
        ctx.roundedRect(...args);
        ctx.stroke();
    }
    function fillRoundedRect(...args) {
        const ctx = this;        
        ctx.beginPath();
        ctx.roundedRect(...args);
        ctx.fill();
    }
    CanvasRenderingContext2D.prototype.roundedRect = roundedRect;
    CanvasRenderingContext2D.prototype.strokeRoundedRect = strokeRoundedRect;
    CanvasRenderingContext2D.prototype.fillRoundedRect = fillRoundedRect;
}
Extend2DRoundedRect();


// Using rounded rectangle extended 2D context
const ctx = canvas.getContext('2d');
ctx.strokeStyle = "#000";
ctx.strokeRoundedRect(10.5, 10.5, 180, 180);      // no radius render rectangle
ctx.strokeRoundedRect(210.5, 10.5, 180, 180, 20); // Draw 1px line along center of pixels
ctx.strokeRoundedRect(20, 20, 160, 160, 30);  
ctx.fillRoundedRect(30, 30, 140, 140, 20);  

ctx.fillRoundedRect(230, 30, 140, 40, 20);  // Circle ends
ctx.fillRoundedRect(230, 80, 140, 20, 20);  // Auto circle ends
ctx.fillRoundedRect(280, 120, 40, 40, 120); // circle all sides

var inset = 0;
ctx.beginPath();
while (inset < 80) {
    ctx.roundedRect(
        10 + inset, 210 + inset, 
        380 - inset * 2, 180 - inset * 2, 
        50 - inset
     );
     inset += 8;
}
ctx.fill("evenodd");
<canvas id="canvas" width="400" height="400"></canvas>
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Note that [`roundRect`](https://github.com/whatwg/html/issues/5619) will probably be part of the specs soon (it's already there in Chrome under `chrome://flags/#enable-experimental-web-platform-features`). You might want to feature-detect and *polyfill* that API instead of writing an other one. Also, to keep the behavior of `fillRect` and `strokeRect`, you might prefer use a Path2D object instead of breaking the context's current path. – Kaiido Mar 24 '21 at 02:20
  • @Kaiido Thanks for heads up. No clash as i use `rounded` not `round` I was hoping for `joinRadius` with `convexJoinRadius` and `concaveJoinRadius` (for closed paths) properties and/or a call similar to `setLineDash` eg `setCornerRadius([r1, r2, ..., rn])` – Blindman67 Mar 24 '21 at 03:05
  • Yes, no clash, it's just for the future, it might become more useful to the readers to just plug that code and use it the same way as the native implementation. And for the joinRadius idea I guess you could open a discussion at https://github.com/fserb/canvas2D/issues I'm not too sure to understand if what you want is to control the radius of the `lineJoin="round"` or something else though. But thanks for the concave point, I now see that Chrome's current implementation doesn't handle concave angles... Sounds like it ought to be there... – Kaiido Mar 24 '21 at 03:50
  • @Kaiido see snippet bottom https://stackoverflow.com/a/44856925/3877726 as example of rounding joins (adds an arc at each join). Its a rather poor implementation as it can be done with a little less math using `arcTo` as in implemented in https://stackoverflow.com/a/63056660/3877726 A global join arc radius would allow for rounded corners at joins involving lines, curves, and arcs, Eg a pie chart with rounded corners looks very nice but the math is complex – Blindman67 Mar 24 '21 at 04:19