How to calculate rounded corners for a polygon?

I'm looking for an algorithm that allows me to create rounded corners from a polygon.

I have an array of points that represents the polygon (outlined in red) and on output I want an array of points that represents the polygon with rounded corners (outlined in black).

I would also like to have a way to control the radius of each corner.

I tried to use Bézier curves and subdivision but it's not what I'm looking for. Bézier curves and subdivision are smoothing the polygon.

What I want is to only make the corners rounded.

Does somebody know any good algorithm for doing that?

I'm working with C# but the code has to be independent from any .NET libraries.

Example


Solution 1:

Some geometry with Paint:


0. You have a corner:
Corner

1. You know the coordinates of corner points, let it be P1, P2 and P:
Points of corner

2. Now you can get vectors from points and angle between vectors:
Vectors and angle

angle = atan(PY - P1Y, PX - P1X) - atan(PY - P2Y, PX - P2X)


3. Get the length of segment between angular point and the points of intersection with the circle.
Segment

segment = PC1 = PC2 = radius / |tan(angle / 2)|


4. Here you need to check the length of segment and the minimal length from PP1 and PP2:
Minimal length
Length of PP1:

PP1 = sqrt((PX - P1X)2 + (PY - P1Y)2)

Length of PP2:

PP2 = sqrt((PX - P2X)2 + (PY - P2Y)2)

If segment > PP1 or segment > PP2 then you need to decrease the radius:

min = Min(PP1, PP2) (for polygon is better to divide this value by 2)
segment > min ?
    segment = min
    radius = segment * |tan(angle / 2)|


5. Get the length of PO:

PO = sqrt(radius2 + segment2)


6. Get the C1X and C1Y by the proportion between the coordinates of the vector, length of vector and the length of the segment:
Coordinates of PC1

Proportion:

(PX - C1X) / (PX - P1X) = PC1 / PP1

So:

C1X = PX - (PX - P1X) * PC1 / PP1

The same for C1Y:

C1Y = PY - (PY - P1Y) * PC1 / PP1


7. Get the C2X and C2Y by the same way:

C2X = PX - (PX - P2X) * PC2 / PP2
C2Y = PY - (PY - P2Y) * PC2 / PP2


8. Now you can use the addition of vectors PC1 and PC2 to find the centre of circle by the same way by proportion:
Addition of vectors

(PX - OX) / (PX - CX) = PO / PC
(PY - OY) / (PY - CY) = PO / PC

Here:

CX = C1X + C2X - PX
CY = C1Y + C2Y - PY
PC = sqrt((PX - CX)2 + (PY - CY)2)

Let:

dx = PX - CX = PX * 2 - C1X - C2X
dy = PY - CY = PY * 2 - C1Y - C2Y

So:

PC = sqrt(dx2 + dy2)

OX = PX - dx * PO / PC
OY = PY - dy * PO / PC


9. Here you can draw an arc. For this you need to get start angle and end angle of arc:
Arc
Found it here:

startAngle = atan((C1Y - OY) / (C1X - OX))
endAngle = atan((C2Y - OY) / (C2X - OX))


10. At last you need to get a sweep angle and make some checks for it:
Sweep angle

sweepAngle = endAngle - startAngle

If sweepAngle < 0 then swap startAngle and endAngle, and invert sweepAngle:

sweepAngle < 0 ?    
    sweepAngle = - sweepAngle
    startAngle = endAngle

Check if sweepAngle > 180 degrees:

sweepAngle > 180 ?    
    sweepAngle = 180 - sweepAngle


11. And now you can draw a rounded corner:
The result

Some geometry with c#:

private void DrawRoundedCorner(Graphics graphics, PointF angularPoint, 
                                PointF p1, PointF p2, float radius)
{
    //Vector 1
    double dx1 = angularPoint.X - p1.X;
    double dy1 = angularPoint.Y - p1.Y;

    //Vector 2
    double dx2 = angularPoint.X - p2.X;
    double dy2 = angularPoint.Y - p2.Y;

    //Angle between vector 1 and vector 2 divided by 2
    double angle = (Math.Atan2(dy1, dx1) - Math.Atan2(dy2, dx2)) / 2;

    // The length of segment between angular point and the
    // points of intersection with the circle of a given radius
    double tan = Math.Abs(Math.Tan(angle));
    double segment = radius / tan;

    //Check the segment
    double length1 = GetLength(dx1, dy1);
    double length2 = GetLength(dx2, dy2);

    double length = Math.Min(length1, length2);

    if (segment > length)
    {
        segment = length;
        radius = (float)(length * tan);
    }

    // Points of intersection are calculated by the proportion between 
    // the coordinates of the vector, length of vector and the length of the segment.
    var p1Cross = GetProportionPoint(angularPoint, segment, length1, dx1, dy1);
    var p2Cross = GetProportionPoint(angularPoint, segment, length2, dx2, dy2);

    // Calculation of the coordinates of the circle 
    // center by the addition of angular vectors.
    double dx = angularPoint.X * 2 - p1Cross.X - p2Cross.X;
    double dy = angularPoint.Y * 2 - p1Cross.Y - p2Cross.Y;

    double L = GetLength(dx, dy);
    double d = GetLength(segment, radius);

    var circlePoint = GetProportionPoint(angularPoint, d, L, dx, dy);

    //StartAngle and EndAngle of arc
    var startAngle = Math.Atan2(p1Cross.Y - circlePoint.Y, p1Cross.X - circlePoint.X);
    var endAngle = Math.Atan2(p2Cross.Y - circlePoint.Y, p2Cross.X - circlePoint.X);

    //Sweep angle
    var sweepAngle = endAngle - startAngle;

    //Some additional checks
    if (sweepAngle < 0)
    {
        startAngle = endAngle;
        sweepAngle = -sweepAngle;
    }

    if (sweepAngle > Math.PI)
        sweepAngle = Math.PI - sweepAngle;

    //Draw result using graphics
    var pen = new Pen(Color.Black);

    graphics.Clear(Color.White);
    graphics.SmoothingMode = SmoothingMode.AntiAlias;

    graphics.DrawLine(pen, p1, p1Cross);
    graphics.DrawLine(pen, p2, p2Cross);

    var left = circlePoint.X - radius;
    var top = circlePoint.Y - radius;
    var diameter = 2 * radius;
    var degreeFactor = 180 / Math.PI;

    graphics.DrawArc(pen, left, top, diameter, diameter, 
                     (float)(startAngle * degreeFactor), 
                     (float)(sweepAngle * degreeFactor));
}

private double GetLength(double dx, double dy)
{
    return Math.Sqrt(dx * dx + dy * dy);
}

private PointF GetProportionPoint(PointF point, double segment, 
                                  double length, double dx, double dy)
{
    double factor = segment / length;

    return new PointF((float)(point.X - dx * factor), 
                      (float)(point.Y - dy * factor));
}

To get points of arc you can use this:

//One point for each degree. But in some cases it will be necessary 
// to use more points. Just change a degreeFactor.
int pointsCount = (int)Math.Abs(sweepAngle * degreeFactor);
int sign = Math.Sign(sweepAngle);

PointF[] points = new PointF[pointsCount];

for (int i = 0; i < pointsCount; ++i)
{
    var pointX = 
       (float)(circlePoint.X  
               + Math.Cos(startAngle + sign * (double)i / degreeFactor)  
               * radius);

    var pointY = 
       (float)(circlePoint.Y 
               + Math.Sin(startAngle + sign * (double)i / degreeFactor) 
               * radius);

    points[i] = new PointF(pointX, pointY);
}

Solution 2:

You are looking for an arc tangent to two connected line segments, of a given radius, given by some sequential array of points. The algorithm to find this arc is as follows:

  1. For each segment, construct a normal vector.

    1. If you are working in 2d, you can just subtract the two endpoints to get a tangent vector (X, Y). In that case, normal vectors will be plus or minus (-Y, X). Normalize the normal vector to length one. Finally, choose the direction with a positive dot product with the tangent vector of the next segment. (See update below).

    2. If you are working in 3d not 2d, to get the normal, cross the tangent vectors of the two segments at the vertex you wish to round to get a perpendicular vector to the plane of the lines. If the perpendicular has length zero, the segments are parallel and no round can be required. Otherwise, normalize it, then cross the perpendicular with the tangent to get the normal.)

  2. Using the normal vectors, offset each line segment towards the interior of the polygon by your desired radius. To offset a segment, offset its endpoints using the normal vector N you just computed, like so: P' = P + r * N (a linear combination).

  3. Intersect the two offset lines to find the center. (This works because a radius vector of a circle is always perpendicular to its tangent.)

  4. To find the point at which the circle intersects each segment, offset the circle center backwards to each original segment. These will be the endpoints of your arc.

  5. Make sure the arc endpoints are inside each segment, otherwise you will be creating a self-intersecting polygon.

  6. Create an arc through both endpoints with center & radius you determined.

I don't have any proper drafting software at hand, but this diagram sort of shows the idea:

enter image description here

At this point you will need either to introduce classes to represent a figure consisting of line and arc segments, or polygonize the arc to an appropriate accuracy and add all the segments to the polygon.

Update: I have updated the image, labeling the points P1, P2, and P3, and normal vectors Norm12 and Norm23. The normalized normals are unique only up to flipping direction, and you should choose the flips as follows:

  • The dot product of Norm12 with (P3 - P2) must be positive. If it is negative, multiple Norm12 by -1.0. If it is zero, the points are collinear and no rounded corner need be created. This is because you want to offset towards P3.

  • The dot product of Norm23 with (P1 - P2) must also be positive since you are offsetting towards P1.

Solution 3:

Objective-C adaptation of nempoBu4 answer:

typedef enum {
    path_move_to,
    path_line_to
} Path_command;





static inline CGFloat sqr (CGFloat a)
{
    return a * a;
}





static inline CGFloat positive_angle (CGFloat angle)
{
    return angle < 0 ? angle + 2 * (CGFloat) M_PI : angle;
}





static void add_corner (UIBezierPath* path, CGPoint p1, CGPoint p, CGPoint p2, CGFloat radius, Path_command first_add)
{
    // 2
    CGFloat angle = positive_angle (atan2f (p.y - p1.y, p.x - p1.x) - atan2f (p.y - p2.y, p.x - p2.x));

    // 3
    CGFloat segment = radius / fabsf (tanf (angle / 2));
    CGFloat p_c1 = segment;
    CGFloat p_c2 = segment;

    // 4
    CGFloat p_p1 = sqrtf (sqr (p.x - p1.x) + sqr (p.y - p1.y));
    CGFloat p_p2 = sqrtf (sqr (p.x - p2.x) + sqr (p.y - p2.y));
    CGFloat min = MIN(p_p1, p_p2);
    if (segment > min) {
        segment = min;
        radius = segment * fabsf (tanf (angle / 2));
    }

    // 5
    CGFloat p_o = sqrtf (sqr (radius) + sqr (segment));

    // 6
    CGPoint c1;
    c1.x = (CGFloat) (p.x - (p.x - p1.x) * p_c1 / p_p1);
    c1.y = (CGFloat) (p.y - (p.y - p1.y) * p_c1 / p_p1);

    //  7
    CGPoint c2;
    c2.x = (CGFloat) (p.x - (p.x - p2.x) * p_c2 / p_p2);
    c2.y = (CGFloat) (p.y - (p.y - p2.y) * p_c2 / p_p2);

    // 8
    CGFloat dx = p.x * 2 - c1.x - c2.x;
    CGFloat dy = p.y * 2 - c1.y - c2.y;

    CGFloat p_c = sqrtf (sqr (dx) + sqr (dy));

    CGPoint o;
    o.x = p.x - dx * p_o / p_c;
    o.y = p.y - dy * p_o / p_c;

    // 9
    CGFloat start_angle = positive_angle (atan2f ((c1.y - o.y), (c1.x - o.x)));
    CGFloat end_angle = positive_angle (atan2f ((c2.y - o.y), (c2.x - o.x)));


    if (first_add == path_move_to) {
        [path moveToPoint: c1];
    }
    else {
        [path addLineToPoint: c1];
    }
    [path addArcWithCenter: o radius: radius startAngle: start_angle endAngle: end_angle clockwise: angle < M_PI];
}





UIBezierPath* path_with_rounded_corners (NSArray<NSValue*>* points, CGFloat corner_radius)
{
    UIBezierPath* path = [UIBezierPath bezierPath];
    NSUInteger count = points.count;
    for (NSUInteger i = 0; i < count; ++i) {
        CGPoint prev = points[i > 0 ? i - 1 : count - 1].CGPointValue;
        CGPoint p = points[i].CGPointValue;
        CGPoint next = points[i + 1 < count ? i + 1 : 0].CGPointValue;
        add_corner (path, prev, p, next, corner_radius, i == 0 ? path_move_to : path_line_to);
    }
    [path closePath];
    return path;
}

Solution 4:

I can offer a simple and very calculable and programmable approach that arguably uses optimally few calculations -- of note "only" 3 square roots and no inverse trigonometric functions.

Since this is Stack Overflow and since I have verified this by actual computing using JavaScript and SVG, I will use ECMAScript (JavaScript) programming language to aid in explaining the solution.

Let's assume some corner you want to "round" is made up of known points A, B and C, with B being "the corner".

Points A, B and C with lines from A to B and from B to C; a circle that includes the arc for the rounded corner, at point O as its center, line perpendicular to the vector AB going from O to meet some point F between A and B

The solution can be described by the following steps:

  1. Calculate the length of the BF vector.

    The length equals to the radius (FO) of your circle (which you obviously choose yourself and thus know) divided by the tangent of the angle between vectors BF and BO. This is obviously because the triangle made by points B, O and F is a 'right' triangle (the angle between vectors BF and FO is 90 degrees).

    The angle between vectors BF and BO is half the angle between vectors BA and BC. This may or may not sound obvious, rest assured it's trivially provable but I omit the proof.

    The relationship between the angles is useful because there happens to be a fairly simple equation expressing the relationship between the tangent of an angle and the cosine of twice the angle: Math.tan(a/2) == Math.sqrt((1 - Math.cos(a)) / (1 + Math.cos(a)).

    And it so happens that the cosine of the angle between vectors BA and BC (Math.cos(a)) is the dot product of the two vectors divided by the product of their lengths (see definition of vector dot product on Wikipedia).

    And so, having calculated the cosine of the angle, you can then calculate the tangent of the half angle, and, subsequently, the length of BF:

    (Legend: I model vectors (BA, BC, etc) as objects with properties x and y for their respective coordinates in screen space (X increases to the right, Y downwards); radius is the desired radius of the would-be rounded corner, and BF_length is the length of BF (obviously))

    /// Helper functions
    const length = v => Math.sqrt(v.x * v.x + v.y * v.y);
    const dot_product = (v1, v2) => v1.x * v2.x + v1.y * v2.y;
    const cosine_between = (v1, v2) => dot_product(v1, v2) / (length(v1) * length(v2));
    
    const cos_a = cosine_between(BA, BC);
    const tan_half_a = Math.sqrt((1 - cos_a) / (1 + cos_a));
    const BF_length = radius / tan_half_a;
    
  2. Compute the BF vector. We know its length now (BF_length above) and since BF lies on the same line the vector BA lies on, the former (and, by implication, the coordinate of the point F relative to point B) is computable by doing a scalar multiplication of the length of BF by the unit vector equivalent of BA:

    /// Helper functions
    const unit = v => {
        const l = length(v);
        return { x: v.x / l, y: v.y / l };
    };
    const scalar_multiply = (v, n) => ({ x: v.x * n, y: v.y * n });
    
    const BF = scalar_multiply(unit(BA), BF_length);
    
  3. Now that you have coordinates of F from the prior step, you calculate the FO vector, or the O coordinate. This is done by rotating some vector of length radius that lies on the same line that the vector BA lies on, both vectors pointing in the same direction, by 90 degrees, and moving it so it starts at F.

    Now, whether the rotation is clockwise or counter-clockwise depends on the sign of the angle between the vectors BA and BC, more concretely if the difference between the angles of BA and BC is positive then the rotation is counter-clockwise, otherwise it's clockwise.

    We don't want to calculate angles if we can avoid it -- it's the sign of the difference we want, after all. Long story short the sign of the angle (sign) can be calculated with the expression Math.sign(BA.x * BC.y - BA.y * BC.x).

    Here is computation of coordinates of O (O), with F being the coordinate of well, F:

    /// Helper functions
    const add = (v1, v2) => ({ x: v1.x + v2.x, y: v1.y + v2.y });
    const rotate_by_90_degrees = (v, sign) => ({ x: -v.y * sign, y: v.x * sign });
    
    const sign = Math.sign(BA.x * BC.y - BA.y * BC.x);
    const O = add(F, rotate_by_90_degrees(scalar_multiply(unit(BA), radius), sign));
    
    

That's all -- since you've obtained the point O with coordinates in the same space as those of your original points (A, B and C), you can just put a circle of the used radius with O as its centre.

This may be obvious to most making use of this answer, but to be on the safe side: please keep in mind that in this answer I would normally refer to vectors and coordinates as the same kind of measure -- a vector has arity which is amount of components it has; for a 2-dimensional system of coordinates, the arity is obviously 2. A vector object thus does not specifically encode its "start", only "end" -- since there is just two components, the implication is that the vector "starts" at the coordinate system origin. The vector BA, for instance is indeed the vector between points B and A, but since the program stores only two components for the vector (x and y in the snippets), it is as if the vector was moved so that the point B is now at the origin of the coordinate system. A point also consists of two components, so "vector" and "point" are interchangeable. You have to understand this very clearly, otherwise some calculations I have offered may seem strange at times. It may be easier if you just think of vectors in this answer as "one dimensional" arrays with two elements each. In fact this is how I originally programmed these, but I switched to objects with x and y properties for the sake of illustrating the solution with code.

Computing the corresponding circular arc from points F and some F' (its equivalent on the BC vector) should be fairly easy, everything considered, but I am not including it unless someone expresses a wish for it.

Solution 5:

Here is my realisation of dbc's idea on c#:

/// <summary>
/// Round polygon corners
/// </summary>
/// <param name="points">Vertices array</param>
/// <param name="radius">Round radius</param>
/// <returns></returns>
static public GraphicsPath RoundCorners(PointF[] points, float radius) {
    GraphicsPath retval = new GraphicsPath();
    if (points.Length < 3) {
        throw new ArgumentException();
    }
    rects = new RectangleF[points.Length];
    PointF pt1, pt2;
    //Vectors for polygon sides and normal vectors
    Vector v1, v2, n1 = new Vector(), n2 = new Vector();
    //Rectangle that bounds arc
    SizeF size = new SizeF(2 * radius, 2 * radius);
    //Arc center
    PointF center = new PointF();

    for (int i = 0; i < points.Length; i++) {
        pt1 = points[i];//First vertex
        pt2 = points[i == points.Length - 1 ? 0 : i + 1];//Second vertex
        v1 = new Vector(pt2.X, pt2.Y) - new Vector(pt1.X, pt1.Y);//One vector
        pt2 = points[i == 0 ? points.Length - 1 : i - 1];//Third vertex
        v2 = new Vector(pt2.X, pt2.Y) - new Vector(pt1.X, pt1.Y);//Second vector
        //Angle between vectors
        float sweepangle = (float)Vector.AngleBetween(v1, v2);
        //Direction for normal vectors
        if (sweepangle < 0) { 
            n1 = new Vector(v1.Y, -v1.X);
            n2 = new Vector(-v2.Y, v2.X);
        }
        else {
            n1 = new Vector(-v1.Y, v1.X);
            n2 = new Vector(v2.Y, -v2.X);
        }

        n1.Normalize(); n2.Normalize();
        n1 *= radius; n2 *= radius;
        /// Points for lines which intersect in the arc center
        PointF pt = points[i];
        pt1 = new PointF((float)(pt.X + n1.X), (float)(pt.Y + n1.Y));
        pt2 = new PointF((float)(pt.X + n2.X), (float)(pt.Y + n2.Y));
        double m1 = v1.Y / v1.X, m2 = v2.Y / v2.X;
        //Arc center
        if (v1.X == 0) {// first line is parallel OY
            center.X = pt1.X;
            center.Y = (float)(m2 * (pt1.X - pt2.X) + pt2.Y);
        }
        else if (v1.Y == 0) {// first line is parallel OX
            center.X = (float)((pt1.Y - pt2.Y) / m2 + pt2.X);
            center.Y = pt1.Y;
        }
        else if (v2.X == 0) {// second line is parallel OY
            center.X = pt2.X;
            center.Y = (float)(m1 * (pt2.X - pt1.X) + pt1.Y);
        }
        else if (v2.Y == 0) {//second line is parallel OX
            center.X = (float)((pt2.Y - pt1.Y) / m1 + pt1.X);
            center.Y = pt2.Y;
        }
        else {
            center.X = (float)((pt2.Y - pt1.Y + m1 * pt1.X - m2 * pt2.X) / (m1 - m2));
            center.Y = (float)(pt1.Y + m1 * (center.X - pt1.X));
        }
        rects[i] = new RectangleF(center.X - 2, center.Y - 2, 4, 4);
        //Tangent points on polygon sides
        n1.Negate(); n2.Negate();
        pt1 = new PointF((float)(center.X + n1.X), (float)(center.Y + n1.Y));
        pt2 = new PointF((float)(center.X + n2.X), (float)(center.Y + n2.Y));
        //Rectangle that bounds tangent arc
        RectangleF rect = new RectangleF(new PointF(center.X - radius, center.Y - radius), size);
        sweepangle = (float)Vector.AngleBetween(n2, n1);
        retval.AddArc(rect, (float)Vector.AngleBetween(new Vector(1, 0), n2), sweepangle);
    }
    retval.CloseAllFigures();
    return retval;
}