skip to Main Content

I’m trying to draw a fairly basic bird shape. I’m using the circle for the body and I’m trying to draw the upper beak (highlighted in red) like shown in the reference image below.

Image reference:

enter image description here

struct Beak: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.width/4, y: rect.height/2))
        path.addLine(to: CGPoint(x: 0, y: rect.height/2))
        // draw a line back to the circumference at a given angle
        // & close it with an arc along the circumference of the Body circle
        path.addLine(to: ??) 
        return path
    }
}

struct BirdView: View {
    var body: some View {
        ZStack {
            // MARK: Body
            Circle()
                .stroke(lineWidth: 10)
                .frame(width: 150)
                .overlay(alignment: .center) {
                    Circle()
                        .fill(.yellow)
                }
            
            // MARK: Beak
            Beak()
                .stroke(lineWidth: 5)
            
        }
        .frame(width: 300, height: 300)
        .background(.orange)
    }
}

What I’m trying to achieve is for the beak to also close with an arc along the circumference of the body circle so that I can animate it later with rotation to simulate the beak open/close. Any help is appreciated.

2

Answers


  1. Instead of doing the math to figure out how to draw the arc, I would just use subtracting to "subtract" the circle from a right-angled triangle.

    struct Beak: Shape {
        let radius: CGFloat // this is the radius of the circle to be subtracted
        let angle: Angle
        func path(in rect: CGRect) -> Path {
            // first draw a right-angled triangle where the two short edges
            // intersect at the middle of rect
            var path = Path()
            path.move(to: CGPoint(x: rect.midX, y: rect.midY))
            path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
            let ratio = tan(angle.radians)
            let height = ratio * rect.width / 2
            path.addLine(to: CGPoint(x: rect.midX, y: rect.midY - height))
            path.closeSubpath()
    
            // then create the circle
            let circle = Path(ellipseIn: CGRect(
                x: rect.midX - radius,
                y: rect.midY - radius,
                width: radius * 2,
                height: radius * 2
            ))
            return path.subtracting(circle)
        }
    }
    

    Then the beak and body can be combined like this:

    var body: some View {
        ZStack {
            // MARK: Body
            Circle()
                .fill(.yellow)
                .overlay(alignment: .center) {
                    Circle()
                        .stroke(lineWidth: 5)
                }
                .frame(width: 150)
            
            // MARK: Beak
            Beak(radius: 75, angle: .degrees(16))
                .stroke(style: .init(lineWidth: 5, lineJoin: .round))
                .padding(5)
            
        }
        .frame(width: 300, height: 300)
        .background(.orange)
    }
    

    Notes:

    • I put the stroked circle above the filled circle to make it easier to reason about where the circle’s stroke is, and how thick it is, making aligning the strokes of the two paths easier.
    • I used a .round line join to stroke the beak, because a miter join looks terrible
    • I added some padding around the beak, so that the stroke doesn’t go beyond the orange background.

    Output:

    enter image description here

    Login or Signup to reply.
  2. If you use .addArc to draw the curved bit then there’s not much to it. It’s perhaps easier if you use the middle point for the start of the arc and then move the final shape into position using .offset. Also, if you apply it as an overlay to the Circle then you can base the size of the beak on the size of the circle.

    Here’s the adapted Shape:

    struct Beak: Shape {
        let angleDegrees: CGFloat
        func path(in rect: CGRect) -> Path {
            var path = Path()
            path.move(to: CGPoint(x: rect.midX, y: rect.midY))
            path.addArc(
                center: CGPoint(x: rect.maxX, y: rect.midY),
                radius: rect.midX,
                startAngle: .degrees(180),
                endAngle: .degrees(180 + angleDegrees),
                clockwise: angleDegrees < 0
            )
            path.addLine(to: CGPoint(x: 0, y: rect.midY))
            path.closeSubpath()
            return path
        }
    }
    

    And here’s how you can use it. I couldn’t resist animating itπŸ˜„

    struct BirdView: View {
        @State private var gapeAngle = Angle.zero
        var body: some View {
            ZStack {
                // MARK: Body
                Circle()
                    .stroke(lineWidth: 5)
                    .frame(width: 150)
                    .background {
                        Circle()
                            .fill(.yellow)
                    }
                    .overlay {
    
                        // Eye
                        Ellipse()
                            .fill(.black)
                            .frame(width: 30, height: 27)
                            .offset(x: -32, y: -28)
                    }
                    .overlay {
    
                        // Upper beak
                        Beak(angleDegrees: 20)
                            .stroke(style: .init(lineWidth: 5, lineJoin: .round))
                            .foregroundStyle(.red)
                            .offset(x: -75)
                            .rotationEffect(gapeAngle)
                    }
                    .overlay {
    
                        // Lower beak
                        Beak(angleDegrees: -20)
                            .stroke(style: .init(lineWidth: 5, lineJoin: .round))
                            .foregroundStyle(.red)
                            .offset(x: -75)
                            .rotationEffect(-gapeAngle)
                    }
            }
            .frame(width: 300, height: 300)
            .background(.orange)
            .onAppear {
                withAnimation(.easeInOut.repeatForever(autoreverses: true)) {
                    gapeAngle = .degrees(5)
                }
            }
        }
    }
    

    Animation

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search