UIBezierPath Triangle with rounded edges
Asked Answered
S

8

29

I have designed the following code to generate a UIBezierPath. This path is used within a CAShapeLayer to mask a UIView. Note, the view's height and width are variable.

This code generates a triangle with sharp edges, but I would like to make the corners rounded. I have experimented with addArcWithCenter..., lineCapStyle and lineJoinStyle etc but nothing seems to work for me.

UIBezierPath *bezierPath = [UIBezierPath bezierPath];

CGPoint center = CGPointMake(rect.size.width / 2, 0);
CGPoint bottomLeft = CGPointMake(10, rect.size.height - 0);
CGPoint bottomRight = CGPointMake(rect.size.width - 0, rect.size.height - 0);
 
[bezierPath moveToPoint:center];
[bezierPath addLineToPoint:bottomLeft];
[bezierPath addLineToPoint:bottomRight];
[bezierPath closePath];

How do I round all the edges of a triangle in a UIBezierPath? Do I need sublayers, multiple paths etc?

I am not drawing this BezierPath so all the CGContext... functions in drawRect are not useful in this context

Stantonstanway answered 7/12, 2013 at 13:59 Comment(4)
You don't need sublayers nor multiple paths. Just add addArcWithCenter... or addQuadCurveToPoint... method calls after each line segment. Everything else is just a matter of correct math.Bumblebee
Is your problem constrained to only equilateral triangles, right-angled, triangle or are you looking for a solution for any triangle? What if the height of the triangle is less than the corner radius?Kassia
@DavidRönnqvist any shaped triangle, and the minimum height for the triangle is 75 points, and the maximum corner radius is 15 points. Thanks for the help!Stantonstanway
If you are drawing a equilateral triangle, see https://mcmap.net/q/501110/-how-to-round-corners-of-uiimage-with-hexagon-mask for a generalized solution for drawing an equilateral polygon with rounded corners.Psf
H
128

Edit

FWIW: This answer serves its educational purpose by explaining what CGPathAddArcToPoint(...) does for you. I would highly recommend that you read through it as it will help you understand and appreciate the CGPath API. Then you should go ahead and use that, as seen in an0's answer, instead of this code when you round edges in your app. This code should only be used as a reference if you want to play around with and learn about geometry calculations like this.


Original answer

Because I find questions like this so fun, I had to answer it :)

This is a long answer. There is no short version :D


Note: For my own simplicity, my solution is making some assumptions about the points that are being used to form the triangle such as:

  • The area of the triangle is large enough to fit the rounded corner (e.g. the height of the triangle is greater than the diameter of the circles in the corners. I'm not checking for or trying to prevent any kind of strange results that may happen otherwise.
  • The corners are listed in counter clock-wise order. You could make it work for any order but it felt like a fair enough constraint to add for simplicity.

If you wanted, you could use the same technique to round any polygon as long as it's strictly convex (i.e. not a pointy star). I won't explain how to do it though but it follows the same principle.


It all starts of with a triangle, that you want to round the corners of with some radius, r:

enter image description here

The rounded triangle should be contained in the pointy triangle so the first step is to find the locations, as close to the corners as possible, where you can fit a circle with the radius, r.

A simple way of doing this is to create 3 new lines parallel to the 3 sides in the triangle and shift each of the the distance r inwards, orthogonal to the side of the original side.

To do this you calculate the slope/angle of each line and the offset to apply to the two new points:

CGFloat angle = atan2f(end.y - start.y,
                       end.x - start.x);

CGVector offset = CGVectorMake(-sinf(angle)*radius,
                                cosf(angle)*radius);

Note: for clarity I'm using the CGVector type (available in iOS 7), but you can just as well use a point or a size to work with previous OS versions.

then you add the offset to both start and end points for each line:

CGPoint offsetStart = CGPointMake(start.x + offset.dx,
                                  start.y + offset.dy);

CGPoint offsetEnd   = CGPointMake(end.x + offset.dx,
                                  end.y + offset.dy);

When you do tho you will see that the three lines intersect each other in three places:

enter image description here

Each intersection point is exactly the distance r from two of the sides (assuming that the triangle is large enough, as stated above).

You can calculate the intersection of two lines as:

//       (x1⋅y2-y1⋅x2)(x3-x4) - (x1-x2)(x3⋅y4-y3⋅x4)
// px =  –––––––––––––––––––––––––––––––––––––––––––
//            (x1-x2)(y3-y4) - (y1-y2)(x3-x4)

//       (x1⋅y2-y1⋅x2)(y3-y4) - (y1-y2)(x3⋅y4-y3⋅x4)
// py =  –––––––––––––––––––––––––––––––––––––––––––
//            (x1-x2)(y3-y4) - (y1-y2)(x3-x4)

CGFloat intersectionX = ((x1*y2-y1*x2)*(x3-x4) - (x1-x2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));
CGFloat intersectionY = ((x1*y2-y1*x2)*(y3-y4) - (y1-y2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));

CGPoint intersection = CGPointMake(intersectionX, intersectionY);

where (x1, y1) to (x2, y2) is the first line and (x3, y3) to (x4, y4) is the second line.

If you then put a circle, with the radius r, on each intersection point you can see that it will indeed for the rounded triangle (ignoring the different line widths of the triangle and the circles):

enter image description here

Now to create the rounded triangle you want to create a path that changes from a line to an arc to a line (etc.) on the points where the original triangle is orthogonal to the intersection points. This is also the point where the circles tangent the original triangle.

Knowing the slopes of all 3 sides in the triangle, the corner radius and the center of the circles (the intersection points), the start and stop angle for each rounded corner is the slope of that side - 90 degrees. To group these things together, I created a struct in my code, but you don't have to if you don't want to:

typedef struct {
    CGPoint centerPoint;
    CGFloat startAngle;
    CGFloat endAngle;
} CornerPoint;

To reduce code duplication I created a method for myself that calculates the intersection and the angles for one point given a line from one point, via another, to a final point (it's not closed so it's not a triangle):

enter image description here

The code is as follows (it's really just the code that I've shown above, put together):

- (CornerPoint)roundedCornerWithLinesFrom:(CGPoint)from
                                      via:(CGPoint)via
                                       to:(CGPoint)to
                               withRadius:(CGFloat)radius
{
    CGFloat fromAngle = atan2f(via.y - from.y,
                               via.x - from.x);
    CGFloat toAngle   = atan2f(to.y  - via.y,
                               to.x  - via.x);

    CGVector fromOffset = CGVectorMake(-sinf(fromAngle)*radius,
                                        cosf(fromAngle)*radius);
    CGVector toOffset   = CGVectorMake(-sinf(toAngle)*radius,
                                        cosf(toAngle)*radius);


    CGFloat x1 = from.x +fromOffset.dx;
    CGFloat y1 = from.y +fromOffset.dy;

    CGFloat x2 = via.x  +fromOffset.dx;
    CGFloat y2 = via.y  +fromOffset.dy;

    CGFloat x3 = via.x  +toOffset.dx;
    CGFloat y3 = via.y  +toOffset.dy;

    CGFloat x4 = to.x   +toOffset.dx;
    CGFloat y4 = to.y   +toOffset.dy;

    CGFloat intersectionX = ((x1*y2-y1*x2)*(x3-x4) - (x1-x2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));
    CGFloat intersectionY = ((x1*y2-y1*x2)*(y3-y4) - (y1-y2)*(x3*y4-y3*x4)) / ((x1-x2)*(y3-y4) - (y1-y2)*(x3-x4));

    CGPoint intersection = CGPointMake(intersectionX, intersectionY);

    CornerPoint corner;
    corner.centerPoint = intersection;
    corner.startAngle  = fromAngle - M_PI_2;
    corner.endAngle    = toAngle   - M_PI_2;

    return corner;
}

I then used that code 3 times to calculate the 3 corners:

CornerPoint leftCorner  = [self roundedCornerWithLinesFrom:right
                                                       via:left
                                                        to:top
                                                withRadius:radius];

CornerPoint topCorner   = [self roundedCornerWithLinesFrom:left
                                                       via:top
                                                        to:right
                                                withRadius:radius];

CornerPoint rightCorner = [self roundedCornerWithLinesFrom:top
                                                       via:right
                                                        to:left
                                                withRadius:radius];

Now, having all the necessary data, starts the part where we create the actual path. I'm going to rely on the fact that CGPathAddArc will add a straight line from the current point to the start point to not have to draw those lines myself (this is documented behaviour).

The only point I manually have to calculate is the start point of the path. I choose the start of the lower right corner (no specific reason). From there you just add an arc with the center in the intersection points from the start and end angles:

CGMutablePathRef roundedTrianglePath = CGPathCreateMutable();
// manually calculated start point
CGPathMoveToPoint(roundedTrianglePath, NULL,
                  leftCorner.centerPoint.x + radius*cosf(leftCorner.startAngle),
                  leftCorner.centerPoint.y + radius*sinf(leftCorner.startAngle));
// add 3 arcs in the 3 corners 
CGPathAddArc(roundedTrianglePath, NULL,
             leftCorner.centerPoint.x, leftCorner.centerPoint.y,
             radius,
             leftCorner.startAngle, leftCorner.endAngle,
             NO);
CGPathAddArc(roundedTrianglePath, NULL,
             topCorner.centerPoint.x, topCorner.centerPoint.y,
             radius,
             topCorner.startAngle, topCorner.endAngle,
             NO);
CGPathAddArc(roundedTrianglePath, NULL,
             rightCorner.centerPoint.x, rightCorner.centerPoint.y,
             radius,
             rightCorner.startAngle, rightCorner.endAngle,
             NO);
// close the path
CGPathCloseSubpath(roundedTrianglePath); 

Looking something like this:

enter image description here

The final result without all the support lines, look like this:

enter image description here

Helbona answered 17/12, 2013 at 20:31 Comment(6)
Hi, David. Was hoping to ask you: what kind of prototyping /design software do you use for your drawings for iOS such as those above? Does one exist that allows you to get the points (CGPathApply , not so useful, doesn't give me enough points I need..) on the paths, angles, control points (!: The is the most time-consuming part while adjusting, I find) and one that allows you to design glyphs for CoreText visually before one sets sails on the programming part? I have already tried GIMP and the likes but I didn't find them so user-friendly to use. Thanks in advance.Nihilism
@Nihilism I don't use much prototyping software. The drawings above are actually just screenshots of Core Graphics rendering on the iOS simulator. Occasionally I've used Schwartz to experiment with Quartz and Sketch to create vector graphics. That said: I don't know of any tool that gives you all the points of a path, though you can calculate the points yourself from the control points if you've used CGPathApply.Kassia
Many thanks, David, for response. I haven't downloaded Schwartz, but it looks promising. Don't get me wrong, I enjoy the power of CoreGraphics and using it. Personally, I prefer to draw programmatically than using image-generating SW. It "feels" good to have your own shapes and lines by drawing programmatically. I only have problem with control points, which, as I hope, thru the use of SW, can help me visualize better and hope as time goes I can "see" and formulate better in my mind how to determine control points for my lines.Nihilism
(cont.) As a matter of fact, I had a question posted 2 months ago on CGPathApply and control points (so far no comment nor answers). I will see if I need to update it a little. But will do that tomorrow. It's a bit late now. I already +1'd on this answer long time ago, nice work.Nihilism
I think it would be better to use cubic bezier curves. You'd simply choose the control points to be at the vertices of the triangle and the endpoints to be some distance away from the vertices.Rachellrachelle
@DavidRönnqvist awsome explaination : ). i tried it with star shape and it manipulate star shape so please can you guid me.Galop
C
43

@David's geometry is cool and educational. But you really don't need to go through the whole geometry this way. I'll offer a much simpler code:

CGFloat radius = 20;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, (center.x + bottomLeft.x) / 2, (center.y + bottomLeft.y) / 2);
CGPathAddArcToPoint(path, NULL, bottomLeft.x, bottomLeft.y, bottomRight.x, bottomRight.y, radius);
CGPathAddArcToPoint(path, NULL, bottomRight.x, bottomRight.y, center.x, center.y, radius);
CGPathAddArcToPoint(path, NULL, center.x, center.y, bottomLeft.x, bottomLeft.y, radius);
CGPathCloseSubpath(path);

UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:path];
CGPathRelease(path);

bezierPath is what you need. The key point is that CGPathAddArcToPoint does the geometry for you.

Crammer answered 17/12, 2013 at 23:2 Comment(1)
What's BottomLeft/BottomRight stands for?Shanika
A
28

Rounded Triangle in Swift 4

A playground based on @an0's answer above.

rounded triangle in swift

import UIKit
import PlaygroundSupport
import CoreGraphics

var view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
view.backgroundColor = .black
PlaygroundPage.current.liveView = view

let triangle = CAShapeLayer()
triangle.fillColor = UIColor.systemGreen.cgColor
triangle.path = createRoundedTriangle(width: 220, height: 200, radius: 15)
triangle.position = CGPoint(x: 140, y: 130)
view.layer.addSublayer(triangle)


func createRoundedTriangle(width: CGFloat, height: CGFloat, radius: CGFloat) -> CGPath {
    // Draw the triangle path with its origin at the center.
    let point1 = CGPoint(x: -width / 2, y: height / 2)
    let point2 = CGPoint(x: 0, y: -height / 2)
    let point3 = CGPoint(x: width / 2, y: height / 2)

    let path = CGMutablePath()
    path.move(to: CGPoint(x: 0, y: height / 2))
    path.addArc(tangent1End: point1, tangent2End: point2, radius: radius)
    path.addArc(tangent1End: point2, tangent2End: point3, radius: radius)
    path.addArc(tangent1End: point3, tangent2End: point1, radius: radius)
    path.closeSubpath()

    return path
}
Archidiaconal answered 24/9, 2017 at 13:5 Comment(2)
how about stroking this triangle?Lope
@Lope Set lineWidth and strokeColor properties of the triangle variable.Archidiaconal
H
2

@an0 - Thank you! I'm brand new, so I don't have the reputation to mark as useful or comment, but you saved me a ton of time today.

I know it is goes beyond the scope of the question, but the same logic works with CGContextBeginPath(), CGContextMoveToPoint(), CGContextAddArcToPoint(), and CGContextClosePath() in case anyone needs to use that instead.

If anyone is interested, here's my code for an equilateral triangle pointing right. The triangleFrame is a predefined CGRect and the radius is a predefined CGFloat.

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextBeginPath(context);

CGContextMoveToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMidY(triangleFrame)); CGRectGetMidY(triangleFrame));
CGContextAddArcToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMidY(triangleFrame), CGRectGetMinX(triangleFrame), CGRectGetMinY(triangleFrame), radius);
CGContextAddArcToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMinY(triangleFrame), CGRectGetMaxX(triangleFrame), CGRectGetMidY(triangleFrame), radius);
CGContextAddArcToPoint(context, CGRectGetMaxX(triangleFrame), CGRectGetMidY(triangleFrame), CGRectGetMinX(triangleFrame), CGRectGetMaxY(triangleFrame), radius);
CGContextAddArcToPoint(context, CGRectGetMinX(triangleFrame), CGRectGetMaxY(triangleFrame), CGRectGetMinX(triangleFrame), CGRectGetMidY(triangleFrame), radius);

Of course, each point can be defined more specifically for irregular triangles. You can then CGContextFillPath() or CGContextStrokePath() as desired.

Halfhardy answered 18/12, 2013 at 21:4 Comment(0)
E
1

Rounded Triangle in swift

Base on @invisible squirrel's answer I wrote a UIView extension.

   class TriangleView: UIView { 
      override func draw(_ rect: CGRect) {
         let widthHeight = self.layer.frame.size.height

         let triangle = CAShapeLayer()
         triangle.fillColor = UIColor.black.cgColor
         triangle.path = roundedTriangle(widthHeight: widthHeight)
         triangle.position = CGPoint(x: 0, y: 0)
         self.layer.addSublayer(triangle)
      }

      func roundedTriangle(widthHeight: CGFloat) -> CGPath {
         let point1 = CGPoint(x: widthHeight/2, y:0)
         let point2 = CGPoint(x: widthHeight , y: widthHeight)
         let point3 =  CGPoint(x: 0, y: widthHeight)
    
         let path = CGMutablePath()
  
         path.move(to: CGPoint(x: 0, y: widthHeight))
         path.addArc(tangent1End: point1, tangent2End: point2, radius: 5)
         path.addArc(tangent1End: point2, tangent2End: point3, radius: 5)
         path.addArc(tangent1End: point3, tangent2End: point1, radius: 5)
         path.closeSubpath()
         return path
      }  
   }
Entremets answered 4/2, 2021 at 13:13 Comment(1)
To make this answer more helpful to less skilled readers, it would help if you could edit your answer and add a brief explanation of your code.Decalogue
F
1

For anybody wanting a SwiftUI-based solution, here's a Shape adapted from @Anjali's answer. It can be used anywhere you'd use a regular SwiftUI shape like Circle or Ellipse.

struct Triangle: Shape {
  let cornerRadius: CGFloat
  
  func path(in rect: CGRect) -> Path {
    let width = rect.width
    let height = rect.height
    
    // The triangle's three corners.
    let bottomLeft = CGPoint(x: 0, y: height)
    let bottomRight = CGPoint(x: width, y: height)
    let topMiddle = CGPoint(x: rect.midX, y: 0)
    
    // We'll start drawing from the bottom middle of the triangle,
    // the midpoint between the two lower corners.
    let bottomMiddle = CGPoint(x: rect.midX, y: height)
    
    // Draw three arcs to trace the triangle.
    var path = Path()
    path.move(to: bottomMiddle)
    path.addArc(tangent1End: bottomRight, tangent2End: topMiddle, radius: cornerRadius)
    path.addArc(tangent1End: topMiddle, tangent2End: bottomLeft, radius: cornerRadius)
    path.addArc(tangent1End: bottomLeft, tangent2End: bottomRight, radius: cornerRadius)
    
    return path
  }
}
Flemings answered 27/12, 2023 at 13:23 Comment(1)
Would you know how to make it so only the tip of the Triangle is rounded? Thanks!Masonmasonic
L
-1

Based on @David's answer I wrote a little UIBezierPath extension that adds the same functionality that CGPath has:

extension UIBezierPath {
    // Based on https://mcmap.net/q/477864/-uibezierpath-triangle-with-rounded-edges
    public func addArc(from startPoint: CGPoint,
                       to endPoint: CGPoint,
                       centerPoint: CGPoint,
                       radius: CGFloat,
                       clockwise: Bool) {
        let fromAngle = atan2(centerPoint.y - startPoint.y,
                              centerPoint.x - startPoint.x)
        let toAngle = atan2(endPoint.y - centerPoint.y,
                            endPoint.x - centerPoint.x)

        let fromOffset = CGVector(dx: -sin(fromAngle) * radius,
                                  dy: cos(fromAngle) * radius)
        let toOffset = CGVector(dx: -sin(toAngle) * radius,
                                dy: cos(toAngle) * radius)

        let x1 = startPoint.x + fromOffset.dx
        let y1 = startPoint.y + fromOffset.dy

        let x2 = centerPoint.x + fromOffset.dx
        let y2 = centerPoint.y + fromOffset.dy

        let x3 = centerPoint.x + toOffset.dx
        let y3 = centerPoint.y + toOffset.dy

        let x4 = endPoint.x + toOffset.dx
        let y4 = endPoint.y + toOffset.dy

        let intersectionX =
            ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4))
                / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))
        let intersectionY =
            ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4))
                / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4))

        let intersection = CGPoint(x: intersectionX, y: intersectionY)
        let startAngle = fromAngle - .pi / 2.0
        let endAngle = toAngle - .pi / 2.0

        addArc(withCenter: intersection,
               radius: radius,
               startAngle: startAngle,
               endAngle: endAngle,
               clockwise: clockwise)
    }
}

So with this, the code should be something like:

let path = UIBezierPath()

let center = CGPoint(x: rect.size.width / 2, y: 0)
let bottomLeft = CGPoint(x: 10, y: rect.size.height - 0)
let bottomRight = CGPoint(x: rect.size.width - 0, y: rect.size.height - 0)

path.move(to: center);
path.addArc(from: center,
            to: bottomRight,
            centerPoint: bottomLeft,
            radius: radius,
            clockwise: false)
path.addLine(to: bottomRight)
path.close()
Loyalist answered 15/9, 2017 at 15:6 Comment(0)
T
-2

Is much much more less complicated would be just adding two lines of code:

UIBezierPath *bezierPath = [UIBezierPath bezierPath];
bezierPath.lineCapStyle = kCGLineCapRound;
bezierPath.lineJoinStyle = kCGLineJoinRound;

CGPoint center = CGPointMake(rect.size.width / 2, 0);
... 

EDIT:

To see that corners are rounded you need to make triangle a little less than rect bounds:

    CGPoint center = CGPointMake(rect.size.width / 2, 10);
    CGPoint bottomLeft = CGPointMake(10, rect.size.height - 10);
    CGPoint bottomRight = CGPointMake(rect.size.width - 10, rect.size.height - 10);
Tributary answered 4/12, 2015 at 10:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.