Star with rounded corners in SwiftUI

The Star shape used in the Star cutout has sharp corners. This article shows how to modify the shape to create stars with rounded corners. The radius of the corner can be set as a parameter to specify the rounding of the outer and inner corners of the star.



Star shapes

Here are the starting star shapes as defined in Create a Star cutout shape in SwiftUI. The number of points in the star can be passed in as a parameter and it has a default of 5.

 1struct StarShape: Shape {
 2    var points = 5
 3    var cutout = false
 4    var circle = false
 5    
 6    func Cartesian(length:Double, angle:Double) -> CGPoint {
 7        return CGPoint(x: length * cos(angle),
 8                       y: length * sin(angle))
 9    }
10    
11    func path(in rect: CGRect) -> Path {
12        // centre of the containing rect
13        var center = CGPoint(x: rect.width/2.0, y: rect.height/2.0)
14        // Adjust center down for odd number of sides less than 8
15        if points%2 == 1 && points < 8 && !circle {
16            center = CGPoint(x: center.x, y: center.y * ((Double(points) * (-0.04)) + 1.3))
17        }
18        
19        // radius of a circle that will fit in the rect
20        let outerRadius = (Double(min(rect.width,rect.height)) / 2.0) * 0.9
21        let innerRadius = outerRadius * 0.4
22        let offsetAngle = (Double.pi / Double(points)) + Double.pi/2.0
23        
24        var vertices:[CGPoint] = []
25        for i in 0..<points{
26            // Calculate the angle in Radians
27            let angle1 = (2.0 * Double.pi/Double(points)) * Double(i)  + offsetAngle
28            let outerPoint = Cartesian(length: outerRadius, angle: angle1)
29            vertices.append(CGPoint(x: outerPoint.x + center.x, y: outerPoint.y + center.y))
30            
31            let angle2 = (2.0 * Double.pi/Double(points)) * (Double(i) + 0.5)  + offsetAngle
32            let innerPoint = Cartesian(length: (innerRadius),
33                                       angle: (angle2))
34            vertices.append(CGPoint(x: innerPoint.x + center.x, y: innerPoint.y + center.y))
35        }
36        
37        let path = Path() { path in
38            if cutout {
39                if circle {
40                    path.addPath(Circle().path(in: rect))
41                    
42                } else {
43                    path.addPath(Rectangle().path(in: rect))
44                }
45            }
46            for (n, pt) in vertices.enumerated() {
47                n == 0 ? path.move(to: pt) : path.addLine(to: pt)
48            }
49            path.closeSubpath()
50        }
51        return path
52    }
53}

The star shapes are designed to fill the enclosing frame. Here are examples of 5, 6 and 7 pointed stars.

 1    VStack {
 2        StarShape(points: 5)
 3            .fill(Color.orange)
 4        .frame(width: 150, height: 150, alignment: .center)
 5        
 6        StarShape(points: 6)
 7            .fill(Color.orange)
 8        .frame(width: 150, height: 150, alignment: .center)
 9        
10        StarShape(points: 7)
11            .fill(Color.orange)
12        .frame(width: 150, height: 150, alignment: .center)                
13    }

Star shapes with sharp corners
Star shapes with sharp corners



Use arc to round the corners

The stars above have sharp corners and can seem a little harsh. It would be nice to soften the star shape by rounding the corners in the star. Each point in the star is composed of two straight lines. An arc path can be added at each point to define the round corners. The radius of this arc will determine how round the points are.

 1        VStack {
 2            StarShape()
 3                .stroke(Color.red, lineWidth: 2)
 4                .frame(width: 100, height: 100, alignment: .center)
 5            
 6            HStack(spacing:20) {
 7                ZStack {
 8                    Rectangle()
 9                        .stroke(Color.gray)
10                        .frame(width: 100, height: 100, alignment: .center)
11                    
12                    Path { path in
13                        path.move(to: CGPoint(x: 39.4, y: 40.4))
14                        path.addLine(to: CGPoint(x: 50, y: 10))
15                        path.addLine(to: CGPoint(x: 60.5, y: 40.4))
16                    }
17                    .stroke(Color.red, lineWidth: 2)
18                    .frame(width: 100, height: 100, alignment: .center)
19                }
20                ZStack {
21                    Rectangle()
22                        .stroke(Color.gray)
23                        .frame(width: 100, height: 100, alignment: .center)
24                    
25                    Path { path in
26                        path.move(to: CGPoint(x: 39.4, y: 40.4))
27                        path.addLine(to: CGPoint(x: 45, y: 10))
28                        path.addArc(center: .init(x: 50, y: 10),
29                                    radius: 5,
30                                    startAngle: Angle(radians: Double.pi * (-0.80)),
31                                    endAngle: Angle(radians: Double.pi * (-0.20)),
32                                    clockwise: false
33                        )
34                        path.addLine(to: CGPoint(x: 60.5, y: 40.4))
35                    }
36                    .stroke(Color.red, lineWidth: 2)
37                    .frame(width: 100, height: 100, alignment: .center)
38                }
39                ZStack {
40                    Rectangle()
41                        .stroke(Color.gray)
42                        .frame(width: 100, height: 100, alignment: .center)
43                    
44                    Path { path in
45                        path.move(to: CGPoint(x: 39.4, y: 40.4))
46                        path.addLine(to: CGPoint(x: 48, y: 10))
47                        path.addArc(center: .init(x: 50, y: 10),
48                                    radius: 2,
49                                    startAngle: Angle(radians: Double.pi * (-0.80)),
50                                    endAngle: Angle(radians: Double.pi * (-0.20)),
51                                    clockwise: false
52                        )
53                        path.addLine(to: CGPoint(x: 60.5, y: 40.4))
54                    }
55                    .stroke(Color.red, lineWidth: 2)
56                    .frame(width: 100, height: 100, alignment: .center)
57                }
58            }
59
60            Spacer()
61        }

Add an arc to round the points of the Star shape
Add an arc to round the points of the Star shape



Update Star shape with rounded points

The approach to creating the star shape inside a given frame is to first create a number of segments for the star. The structure for the segment is enough to create a single point in the star. Segment is created using two points and two doubles. The center point is the center of an arc for the rounded curve, this was the original outer point in the star with sharp corners. The angle is the angle that defines the pointing direction of this segment point. The radius is the radius or the rounding arc. Finally, the line2 point is the destination of a straight line after the arc path.

The line point for connecting the previous segment to this one is a computed property based on the rounding radius and the angle of the starpoint. The start and end angles for the arc are equally distributed on either side of the segment point angle. This is set to less than 90 degrees on either side of the line straight through the segment point. More work could be done to calculate the exact angles that would result in accurate tangents to the rounding arc, and these would vary slightly depending on the number of points on the star. For now, the angle is fixed at 81 degrees or 0.45 of PI.

 1struct Segment {
 2    let center: CGPoint
 3    let angle: Double
 4    let radius: Double
 5    let line2: CGPoint
 6    
 7    var line: CGPoint {
 8        get {
 9            let pt = Cartesian(length: radius, angle: arcAngle1)
10            return CGPoint(x: pt.x + center.x, y: pt.y + center.y)
11        }
12    }
13    
14    var arcAngle1: Double {
15        get { self.angle - (Double.pi * (0.45)) }
16    }
17    
18    var arcAngle2: Double {
19        get { self.angle + (Double.pi * (0.45)) }
20    }
21}

The function to convert polar coordinates to cartesian coordinates is moved outside the star shape so it can be used by the segment structure as well as the star shape.

1func Cartesian(length: Double, angle: Double) -> CGPoint {
2    return CGPoint(x: length * cos(angle),
3                   y: length * sin(angle))
4}

StarShape2 is defined so that the original StarShape can be used for comparison. The initial loop through the number of points on the star calculated the angle for each point, as well as the inner and outer points. These points were added to a list of points for the star. In the star shape with rounded corners, the same points are calculated, but the points as well as the outer point angle are used to create a Segment. A list of segments is created for the star.

The path for the Star shape is created by iterating over the list of segments adding a line, an arc and a line for each segment.

 1struct StarShape2: Shape {
 2    var points = 5
 3    var cornerRadius = 3.0
 4    var isCutout = false
 5    var isCircleOutline = false
 6    
 7    func path(in rect: CGRect) -> Path {
 8        // centre of the containing rect
 9        var center = CGPoint(x: rect.width/2.0, y: rect.height/2.0)
10        // Adjust center down for odd number of sides less than 8
11        if points%2 == 1 && points < 8 && !isCircleOutline {
12            center = CGPoint(x: center.x, y: center.y * ((Double(points) * (-0.04)) + 1.3))
13        }
14        
15        // radius of a circle that will fit in the rect
16        let outerRadius = (Double(min(rect.width,rect.height)) / 2.0) * 0.8
17        let innerRadius = outerRadius * 0.4
18        let offsetAngle = Double.pi * (-0.5)
19        
20        var starSegments:[Segment] = []
21        for i in 0..<(points){
22            let angle1 = (2.0 * Double.pi/Double(points)) * Double(i)  + offsetAngle
23            let outerPoint = Cartesian(length: outerRadius, angle: angle1)
24            let angle2 = (2.0 * Double.pi/Double(points)) * (Double(i) + 0.5)  + offsetAngle
25            let innerPoint = Cartesian(length: (innerRadius), angle: (angle2))
26            
27            let segment = Segment(
28                center: CGPoint(x: outerPoint.x + center.x,
29                                y: outerPoint.y + center.y),
30                angle: angle1,
31                radius: cornerRadius,
32                line2: CGPoint(x: innerPoint.x + center.x,
33                               y: innerPoint.y + center.y))
34            starSegments.append(segment)
35        }
36        
37        let path = Path() { path in
38            if isCutout {
39                if isCircleOutline {
40                    path.addPath(Circle().path(in: rect))
41                    
42                } else {
43                    path.addPath(Rectangle().path(in: rect))
44                }
45            }
46            for (n, op) in starSegments.enumerated() {
47                n == 0 ? path.move(to: op.line) : path.addLine(to: op.line)
48                path.addArc(center: op.center,
49                            radius: op.radius,
50                            startAngle: Angle(radians: op.arcAngle1),
51                            endAngle: Angle(radians: op.arcAngle2),
52                            clockwise: false)
53                path.addLine(to: op.line2)
54            }
55            path.closeSubpath()
56        }
57        return path
58    }
59}

A number of stars with different number of points all with the new rounded corners.

 1struct StarShapeView6: View {
 2    var body: some View {
 3        ZStack {
 4            Color(red: 214/255, green: 232/255, blue: 248/255)
 5                .edgesIgnoringSafeArea(.all)
 6            
 7            VStack {
 8                HStack {
 9                    StarShape2()
10                        .fill(Color.blue)
11                        .frame(width: 150, height: 150, alignment: .center)
12                    StarShape2()
13                        .stroke(Color.blue, lineWidth: 3)
14                        .frame(width: 150, height: 150, alignment: .center)
15                }
16                HStack {
17                    StarShape2(points:4)
18                        .fill(Color.blue)
19                        .frame(width: 150, height: 150, alignment: .center)
20                    StarShape2(points:6)
21                        .fill(Color.blue)
22                        .frame(width: 150, height: 150, alignment: .center)
23                }
24                HStack {
25                    StarShape2(points:7)
26                        .fill(Color.blue)
27                        .frame(width: 150, height: 150, alignment: .center)
28                    StarShape2(points:8)
29                        .fill(Color.blue)
30                        .frame(width: 150, height: 150, alignment: .center)
31                }
32                Spacer()
33            }
34        }
35    }
36}

Star shapes with different number of points with rounded points
Star shapes with different number of points with rounded points



How round should the corners be?

The cornerRadius is an optional parameter on StarShape2, which allows for the creation of numerous stars with the same number of points and varying size of corner radius on the arc for the rounded corners. Here are a number of 5-pointed stars with different size radius for the rounded corners.

 1struct StarShapeView7: View {
 2    var body: some View {
 3        ZStack {
 4            Color(red: 214/255, green: 232/255, blue: 248/255)
 5                .edgesIgnoringSafeArea(.all)
 6            
 7            VStack(spacing:20) {
 8                ForEach([2,3,10], id: \.self) { i in
 9                    HStack(spacing:30) {
10                        VStack(spacing:0) {
11                            let r = Double(i)
12                            StarShape2(points: 5, cornerRadius: r)
13                                .fill(Color.blue)
14                                .frame(width: 150, height: 150)
15                            Text("Corner Radius = \(r, specifier: "%.0F")")
16                                .font(.footnote)
17                        }
18
19                        VStack(spacing:0) {
20                            let r = Double(i * 2)
21                            StarShape2(points: 5, cornerRadius: r)
22                                .fill(Color.blue)
23                                .frame(width: 150, height: 150)
24                            Text("Corner Radius = \(r, specifier: "%.0F")")
25                                .font(.footnote)
26                        }
27                    }
28                }
29                Spacer()
30            }
31        }
32    }
33}

It is a matter of opinion as to which looks better when the radius is 2, 3 or 4. But I think we can agree that rounded corner radius of 10 or 20 look ridiculous - or at least, have lost the star look.

Five-pointed stars with different radius for rounded points
Five-pointed stars with different radius for rounded points



Round inner corners in Star shape

Now the inner corners look sharp after rounding the outer corners! Another arc can be used in the inner corners to round them and soften the shape. The parameters to the segment are renamed and extra parameters are added as well as extra computed properties for the arc on the inner corner.

 1struct Segment {
 2    let outerCenter: CGPoint
 3    let outerAngle: Double
 4    let outerRadius: Double
 5    let innerCenter: CGPoint
 6    let innerAngle: Double
 7
 8    var line: CGPoint {
 9        get {
10            let pt = Cartesian(length: outerRadius, angle: outerStartAngle)
11            return CGPoint(x: pt.x + outerCenter.x, y: pt.y + outerCenter.y)
12        }
13    }
14    
15    var line2: CGPoint {
16        get {
17            let pt = Cartesian(length: outerRadius, angle: innerStartAngle)
18            return CGPoint(x: pt.x + innerCenter.x, y: pt.y + innerCenter.y)
19        }
20    }
21    
22    var outerStartAngle: Double {
23        get { self.outerAngle - (Double.pi * (0.45)) }
24    }
25    var outerEndAngle: Double {
26        get { self.outerAngle + (Double.pi * (0.45)) }
27    }
28    
29    var innerStartAngle: Double {
30        get { self.innerAngle - (Double.pi * (0.7)) }
31    }
32    var innerEndAngle: Double {
33        get { self.innerAngle + (Double.pi * (0.7)) }
34    }
35}

StarShape2 is updated to pass in the extra parameters in the creation of each segment. In addition, the creation of the path is updated to add a line, an arc, a line and another arc for each segment in the list of segments. Note the direction of the inner arc is opposite to the direction of the outer arc.

 1struct StarShape2: Shape {
 2    var points = 5
 3    var cornerRadius = 3.0
 4    var isCutout = false
 5    var isCircleOutline = false
 6    
 7    func path(in rect: CGRect) -> Path {
 8        // centre of the containing rect
 9        var center = CGPoint(x: rect.width/2.0, y: rect.height/2.0)
10        // Adjust center down for odd number of sides less than 8
11        if points%2 == 1 && points < 8 && !isCircleOutline {
12            center = CGPoint(x: center.x, y: center.y * ((Double(points) * (-0.04)) + 1.3))
13        }
14        
15        // radius of a circle that will fit in the rect with some padding
16        let outerRadius = (Double(min(rect.width,rect.height)) / 2.0) * 0.9
17        let innerRadius = outerRadius * 0.4
18        let offsetAngle = Double.pi * (-0.5)
19        
20        var starSegments:[Segment] = []
21        for i in 0..<(points){
22            let angle1 = (2.0 * Double.pi/Double(points)) * Double(i)  + offsetAngle
23            let outerPoint = Cartesian(length: outerRadius, angle: angle1)
24            let angle2 = (2.0 * Double.pi/Double(points)) * (Double(i) + 0.5)  + offsetAngle
25            let innerPoint = Cartesian(length: (innerRadius), angle: (angle2))
26            
27            let segment = Segment(
28                outerCenter: CGPoint(x: outerPoint.x + center.x,
29                                y: outerPoint.y + center.y),
30                outerAngle: angle1,
31                outerRadius: cornerRadius,
32                innerCenter: CGPoint(x: innerPoint.x + center.x,
33                                     y: innerPoint.y + center.y),
34                innerAngle: angle2)
35            starSegments.append(segment)
36        }
37        
38        let path = Path() { path in
39            if isCutout {
40                if isCircleOutline {
41                    path.addPath(Circle().path(in: rect))
42                    
43                } else {
44                    path.addPath(Rectangle().path(in: rect))
45                }
46            }
47            for (n, seg) in starSegments.enumerated() {
48                n == 0 ? path.move(to: seg.line) : path.addLine(to: seg.line)
49                path.addArc(center: seg.outerCenter,
50                            radius: seg.outerRadius,
51                            startAngle: Angle(radians: seg.outerStartAngle),
52                            endAngle: Angle(radians: seg.outerEndAngle),
53                            clockwise: false)
54                path.addLine(to: seg.line2)
55                path.addArc(center: seg.innerCenter,
56                            radius: seg.outerRadius,
57                            startAngle: Angle(radians: seg.innerStartAngle),
58                            endAngle: Angle(radians: seg.innerEndAngle),
59                            clockwise: true)
60            }
61            path.closeSubpath()
62        }
63        return path
64    }
65}

The same view shows the updated stars with rounded inner and outer corners.

 1struct StarShapeView6: View {
 2    var body: some View {
 3        ZStack {
 4            Color(red: 214/255, green: 232/255, blue: 248/255)
 5                .edgesIgnoringSafeArea(.all)
 6
 7            VStack {
 8                HStack {
 9                    StarShape2()
10                        .fill(Color.blue)
11                        .frame(width: 150, height: 150, alignment: .center)
12                    StarShape2()
13                        .stroke(Color.blue, lineWidth: 3)
14                        .frame(width: 150, height: 150, alignment: .center)
15                }
16                HStack {
17                    StarShape2(points:4)
18                        .fill(Color.blue)
19                        .frame(width: 150, height: 150, alignment: .center)
20                    StarShape2(points:6)
21                        .fill(Color.blue)
22                        .frame(width: 150, height: 150, alignment: .center)
23                }
24                HStack {
25                    StarShape2(points:7)
26                        .fill(Color.blue)
27                        .frame(width: 150, height: 150, alignment: .center)
28                    StarShape2(points:8)
29                        .fill(Color.blue)
30                        .frame(width: 150, height: 150, alignment: .center)
31                }
32                Spacer()
33            }
34        }
35    }
36}

Star shapes woth rounded inner and outer corners
Star shapes woth rounded inner and outer corners


This is the view with the 5-pointed stars with increasing corner radius sizes. Again, the stars with inner radius 10 or 20 look ridiculous. The inner radius of 20 is overlapping over multiple segments resulting is an interesting design pattern - but, no longer a star.

Five-pointed stars with different radius for inner and outer rounded corners
Five-pointed stars with different radius for inner and outer rounded corners



Star shape variations

Here are a number of stars using the Star Shape:

  • Stars with sharp corners
  • Stars with rounded corners
  • Solid and outline stars
  • Cutout stars
  • Stars with different number of points

The last shape on the bottom right is a 15 pointed star with a rounded corner radius of 14 in a frame that is 300 by 300. This results n the inner rounded corners overlapping and shows the potential of creating many different shapes based on a regular pattern.

 1    VStack {
 2        HStack {
 3            StarShape2(points: 5, cornerRadius: 0, isCutout: false, isCircleOutline: false)
 4                .fill(Color.red)
 5                .frame(width: 300, height: 300, alignment: .center)
 6            StarShape2(points: 5, cornerRadius: 0, isCutout: false, isCircleOutline: false)
 7                .stroke(Color.red, lineWidth: 3)
 8                .frame(width: 300, height: 300, alignment: .center)
 9            StarShape2(points: 5, cornerRadius: 0, isCutout: true, isCircleOutline: false)
10                .fill(Color.red, style: FillStyle(eoFill: true, antialiased: true))
11                .frame(width: 300, height: 300, alignment: .center)
12            StarShape2(points: 5, cornerRadius: 0, isCutout: true, isCircleOutline: true)
13                .fill(Color.red, style: FillStyle(eoFill: true, antialiased: true))
14                .frame(width: 300, height: 300, alignment: .center)
15        }
16        HStack {
17            StarShape2(points: 5, cornerRadius: 4, isCutout: false, isCircleOutline: false)
18                .fill(Color.red)
19                .frame(width: 300, height: 300, alignment: .center)
20            StarShape2(points: 5, cornerRadius: 4, isCutout: false, isCircleOutline: false)
21                .stroke(Color.red, lineWidth: 3)
22                .frame(width: 300, height: 300, alignment: .center)
23            StarShape2(points: 5, cornerRadius: 4, isCutout: true, isCircleOutline: false)
24                .fill(Color.red, style: FillStyle(eoFill: true, antialiased: true))
25                .frame(width: 300, height: 300, alignment: .center)
26            StarShape2(points: 5, cornerRadius: 4, isCutout: true, isCircleOutline: true)
27                .fill(Color.red, style: FillStyle(eoFill: true, antialiased: true))
28                .frame(width: 300, height: 300, alignment: .center)
29        }
30        HStack {
31            StarShape2(points: 5, cornerRadius: 4, isCutout: false, isCircleOutline: false)
32                .fill(Color.red)
33                .frame(width: 300, height: 300, alignment: .center)
34            StarShape2(points: 5, cornerRadius: 14, isCutout: false, isCircleOutline: false)
35                .fill(Color.red)
36                .frame(width: 300, height: 300, alignment: .center)
37            StarShape2(points: 7, cornerRadius: 4, isCutout: false, isCircleOutline: false)
38                .fill(Color.red)
39                .frame(width: 300, height: 300, alignment: .center)
40            StarShape2(points: 15, cornerRadius: 14, isCutout: false, isCircleOutline: false)
41                .fill(Color.red)
42                .frame(width: 300, height: 300, alignment: .center)
43        }
44        Spacer()
45    }

Various Star shapes with sharp corners, round corners and other variations
Various Star shapes with sharp corners, round corners and other variations




Conclusion

This article built on the basic star in Create a Star cutout shape in SwiftUI to add an arc and a line to the path for the star corners to produce a rounded pointed star. It is possible that the star segment could be broken down to just define one arc and line and this could be used for both the inner corner and outer corner. The star shape could expose more properties for customisation such as the radius of the inner circle or a separate rounding radius for the inner corners, so the inner corners could remain sharp while the outer corners are rounded. Another potential change could be to use a value between 0.0 and 1.0 for the corner radius and set this as percentage of the containing frame rather than point size. This would allow the Star shape be used in a variety of sizes without having to adjust the corner radius.

Source code for the StarShape is available on GitHub.