Drawing Paths and Shapes Tutorial

I am working on the developer tutorial.
Drawing and Animation
Drawing Paths and Shapes
https://developer.apple.com/tutorials/swiftui/drawing-paths-and-shapes

Section 2 Step 9

How do you change the shape of the hexagon?
For example, what do I change in the code to add a side, making it a heptagon?

Code Block
struct BadgeBackground: View {
    var body: some View {
        GeometryReader { geometry in
            Path { path in
                var width: CGFloat = min(geometry.size.width, geometry.size.height)
let height = width
let xScale: CGFloat = 0.832
let xOffset = (width * (1.0 - xScale)) / 2.0
width *= xScale
path.move( to: CGPoint(
x: xOffset + width * 0.95,
y: height * (0.20 + HexagonParameters.adjustment)))
HexagonParameters.points.forEach {
path.addLine( to: .init(
x: xOffset + width * $0.useWidth.0 * $0.xFactors.0,
y: height * $0.useHeight.0 * $0.yFactors.0 ) )
path.addQuadCurve( to: .init(
x: xOffset + width * $0.useWidth.1 * $0.xFactors.1,
y: height * $0.useHeight.1 * $0.yFactors.1
), control: .init(
x: xOffset + width * $0.useWidth.2 * $0.xFactors.2,
y: height * $0.useHeight.2 * $0.yFactors.2
                           )
                       )
                   }
               }


Answered by OOPer in 646064022

How do you change the shape of the hexagon? 

To learn how to change the shape, you need to know how the shape is held in HexagonParameters.

Each Segment in points represents parameters for two things:
  • A line representing an edge

  • A curve connecting the current edge to the next edge

In the tutorial, the parameters are given with unknown pre-calculated constants.

For example, what do I change in the code to add a side, making it a heptagon?

As the parameters for hexagon is pre-calculated, you may need to:
  • Pre-calculate the parameters of heptagon by yourself

or
  • Let Swift calculate the parameters

The second is a little bit difficult if you are not familiar with geometry math, but easier than pre-calculating.

Create new Swift file PolygonParameters.swift:
Code Block
import SwiftUI
struct PolygonParameters {
static let numberOfEdges = 7
static let curveFactor: CGFloat = 0.2
struct Segment {
let useWidth: (CGFloat, CGFloat, CGFloat)
let xFactors: (CGFloat, CGFloat, CGFloat)
let useHeight: (CGFloat, CGFloat, CGFloat)
let yFactors: (CGFloat, CGFloat, CGFloat)
}
static let angleOffset: CGFloat = -.pi / 2 // 90 degrees
static func point(for angle: CGFloat) -> CGPoint {
return CGPoint(x: cos(angle+angleOffset), y: sin(angle+angleOffset))
}
static func point(from startAngle: CGFloat, to endAngle: CGFloat, factor: CGFloat) -> CGPoint {
let start = point(for: startAngle)
let end = point(for: endAngle)
return CGPoint(x: start.x + (end.x-start.x)*factor,
y: start.y + (end.y-start.y)*factor)
}
static let points = (0..<numberOfEdges).map {n->Segment in
let prevAngle = (.pi*2/CGFloat(numberOfEdges)) * CGFloat(n-1)
let currAngle = (.pi*2/CGFloat(numberOfEdges)) * CGFloat(n)
let nextAngle = (.pi*2/CGFloat(numberOfEdges)) * CGFloat(n+1)
let control = point(for: currAngle)
let start = point(from: prevAngle, to: currAngle, factor: 1-curveFactor)
let end = point(from: currAngle, to: nextAngle, factor: curveFactor)
let r: CGFloat = 0.5
return Segment(
useWidth: (1.00, 1.00, 1.00),
xFactors: (r*start.x+0.5, r*end.x+0.5, r*control.x+0.5),
useHeight: (1.00, 1.00, 1.00),
yFactors: (r*start.y+0.5, r*end.y+0.5, r*control.y+0.5)
)
}
}


And use it as follows:
Code Block
import SwiftUI
struct Badge: View {
var body: some View {
GeometryReader { geometry in
Path { path in
var width: CGFloat = min(geometry.size.width, geometry.size.height)
let height = width
let xScale: CGFloat = 0.832
let xOffset = (width * (1.0 - xScale)) / 2.0
width *= xScale
let last = PolygonParameters.points.last!
path.move(
to: CGPoint(
x: xOffset + width * last.useWidth.2 * last.xFactors.2,
y: height * last.useHeight.2 * last.yFactors.2
)
)
PolygonParameters.points.forEach {
path.addLine(
to: .init(
x: xOffset + width * $0.useWidth.0 * $0.xFactors.0,
y: height * $0.useHeight.0 * $0.yFactors.0
)
)
path.addQuadCurve(
to: .init(
x: xOffset + width * $0.useWidth.1 * $0.xFactors.1,
y: height * $0.useHeight.1 * $0.yFactors.1
),
control: .init(
x: xOffset + width * $0.useWidth.2 * $0.xFactors.2,
y: height * $0.useHeight.2 * $0.yFactors.2
)
)
}
}
.fill(LinearGradient(
gradient: .init(colors: [Self.gradientStart, Self.gradientEnd]),
startPoint: .init(x: 0.5, y: 0),
endPoint: .init(x: 0.5, y: 0.6)
))
.aspectRatio(1, contentMode: .fit)
}
}
static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
}
struct Badge_Previews: PreviewProvider {
static var previews: some View {
Badge()
}
}


You can experiment what will be drawn with changing numberOfEdges, curveFactor and angleOffset of PolygonParameters.
Drawing is not a real hexagon, but a shape with 6 sides (not same length) and rounded corners.

Adding another side is not so straightforward.
You need to calculate the positions of summits (heptagon has summits sevrer 2 * π / 7 angle ≈ 51.42 °).
So start from the point at top of y axis, and turn that angle to compute next summit (use cos and sin functions for this).

That will let you create 7 segments instead of 6.

I do advise you to look for something simpler, like an octogone…

Note: when you post code, you should indent the code properly, that's much easier to read

Code Block
struct BadgeBackground: View {
var body: some View {
GeometryReader { geometry in
Path { path in
var width: CGFloat = min(geometry.size.width, geometry.size.height)
let height = width
let xScale: CGFloat = 0.832
let xOffset = (width * (1.0 - xScale)) / 2.0
width *= xScale
path.move( to: CGPoint(
x: xOffset + width * 0.95,
y: height * (0.20 + HexagonParameters.adjustment)))
HexagonParameters.points.forEach {
path.addLine( to: .init(
x: xOffset + width * $0.useWidth.0 * $0.xFactors.0,
y: height * $0.useHeight.0 * $0.yFactors.0 ) )
path.addQuadCurve( to: .init(
x: xOffset + width * $0.useWidth.1 * $0.xFactors.1,
y: height * $0.useHeight.1 * $0.yFactors.1
), control: .init(
x: xOffset + width * $0.useWidth.2 * $0.xFactors.2,
y: height * $0.useHeight.2 * $0.yFactors.2
)
)
}
}


Accepted Answer

How do you change the shape of the hexagon? 

To learn how to change the shape, you need to know how the shape is held in HexagonParameters.

Each Segment in points represents parameters for two things:
  • A line representing an edge

  • A curve connecting the current edge to the next edge

In the tutorial, the parameters are given with unknown pre-calculated constants.

For example, what do I change in the code to add a side, making it a heptagon?

As the parameters for hexagon is pre-calculated, you may need to:
  • Pre-calculate the parameters of heptagon by yourself

or
  • Let Swift calculate the parameters

The second is a little bit difficult if you are not familiar with geometry math, but easier than pre-calculating.

Create new Swift file PolygonParameters.swift:
Code Block
import SwiftUI
struct PolygonParameters {
static let numberOfEdges = 7
static let curveFactor: CGFloat = 0.2
struct Segment {
let useWidth: (CGFloat, CGFloat, CGFloat)
let xFactors: (CGFloat, CGFloat, CGFloat)
let useHeight: (CGFloat, CGFloat, CGFloat)
let yFactors: (CGFloat, CGFloat, CGFloat)
}
static let angleOffset: CGFloat = -.pi / 2 // 90 degrees
static func point(for angle: CGFloat) -> CGPoint {
return CGPoint(x: cos(angle+angleOffset), y: sin(angle+angleOffset))
}
static func point(from startAngle: CGFloat, to endAngle: CGFloat, factor: CGFloat) -> CGPoint {
let start = point(for: startAngle)
let end = point(for: endAngle)
return CGPoint(x: start.x + (end.x-start.x)*factor,
y: start.y + (end.y-start.y)*factor)
}
static let points = (0..<numberOfEdges).map {n->Segment in
let prevAngle = (.pi*2/CGFloat(numberOfEdges)) * CGFloat(n-1)
let currAngle = (.pi*2/CGFloat(numberOfEdges)) * CGFloat(n)
let nextAngle = (.pi*2/CGFloat(numberOfEdges)) * CGFloat(n+1)
let control = point(for: currAngle)
let start = point(from: prevAngle, to: currAngle, factor: 1-curveFactor)
let end = point(from: currAngle, to: nextAngle, factor: curveFactor)
let r: CGFloat = 0.5
return Segment(
useWidth: (1.00, 1.00, 1.00),
xFactors: (r*start.x+0.5, r*end.x+0.5, r*control.x+0.5),
useHeight: (1.00, 1.00, 1.00),
yFactors: (r*start.y+0.5, r*end.y+0.5, r*control.y+0.5)
)
}
}


And use it as follows:
Code Block
import SwiftUI
struct Badge: View {
var body: some View {
GeometryReader { geometry in
Path { path in
var width: CGFloat = min(geometry.size.width, geometry.size.height)
let height = width
let xScale: CGFloat = 0.832
let xOffset = (width * (1.0 - xScale)) / 2.0
width *= xScale
let last = PolygonParameters.points.last!
path.move(
to: CGPoint(
x: xOffset + width * last.useWidth.2 * last.xFactors.2,
y: height * last.useHeight.2 * last.yFactors.2
)
)
PolygonParameters.points.forEach {
path.addLine(
to: .init(
x: xOffset + width * $0.useWidth.0 * $0.xFactors.0,
y: height * $0.useHeight.0 * $0.yFactors.0
)
)
path.addQuadCurve(
to: .init(
x: xOffset + width * $0.useWidth.1 * $0.xFactors.1,
y: height * $0.useHeight.1 * $0.yFactors.1
),
control: .init(
x: xOffset + width * $0.useWidth.2 * $0.xFactors.2,
y: height * $0.useHeight.2 * $0.yFactors.2
)
)
}
}
.fill(LinearGradient(
gradient: .init(colors: [Self.gradientStart, Self.gradientEnd]),
startPoint: .init(x: 0.5, y: 0),
endPoint: .init(x: 0.5, y: 0.6)
))
.aspectRatio(1, contentMode: .fit)
}
}
static let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
static let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
}
struct Badge_Previews: PreviewProvider {
static var previews: some View {
Badge()
}
}


You can experiment what will be drawn with changing numberOfEdges, curveFactor and angleOffset of PolygonParameters.
Ok, now I realize that the adjustable parameters are in a completely different view titled HexagonParameters. You create it later in the tutorial.
Drawing Paths and Shapes Tutorial
 
 
Q