Reputation: 99
Trying to find out the best way to add a radius/slightly rounded corners to this diamond shape code. Have tried a few things yielding some pretty odd results. Any help is greatly appreaciated.
struct DiamondShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
path.move(to: CGPoint(x: center.x, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: center.y))
path.addLine(to: CGPoint(x: center.x, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: center.y))
path.addLine(to: CGPoint(x: center.x, y: 0))
return path
}
}
Upvotes: 1
Views: 255
Reputation: 21720
I don't think there is a simple SwiftUI modifier that makes it possible to round the corners of an arbitrary shape. Instead, it is necessary to add the curves to the path yourself.
Here are some ways to do this:
In the case of a diamond shape, it is not too difficult to add the curves. The following principles describe a simple approach:
Here is an updated version of DiamondShape
that works this way:
struct RoundedDiamond: Shape {
var cornerRadius: CGFloat = 10
func path(in rect: CGRect) -> Path {
var path = Path()
let doubleSideLen = ((rect.width * rect.width) + (rect.height * rect.height)).squareRoot()
let xOffset = cornerRadius * (rect.width / doubleSideLen)
let yOffset = cornerRadius * (rect.height / doubleSideLen)
var x = rect.midX - xOffset
var y = rect.minY + yOffset
path.move(to: CGPoint(x: x, y: y))
x += (2 * xOffset)
path.addQuadCurve(to: CGPoint(x: x, y: y), control: CGPoint(x: rect.midX, y: rect.minY))
x = rect.maxX - xOffset
y = rect.midY - yOffset
path.addLine(to: CGPoint(x: x, y: y))
y += (2 * yOffset)
path.addQuadCurve(to: CGPoint(x: x, y: y), control: CGPoint(x: rect.maxX, y: rect.midY))
x = rect.midX + xOffset
y = rect.maxY - yOffset
path.addLine(to: CGPoint(x: x, y: y))
x -= (2 * xOffset)
path.addQuadCurve(to: CGPoint(x: x, y: y), control: CGPoint(x: rect.midX, y: rect.maxY))
x = rect.minX + xOffset
y = rect.midY + yOffset
path.addLine(to: CGPoint(x: x, y: y))
y -= (2 * yOffset)
path.addQuadCurve(to: CGPoint(x: x, y: y), control: CGPoint(x: rect.minX, y: rect.midY))
path.closeSubpath()
return path
}
}
Some test cases:
HStack {
RoundedDiamond()
.frame(width: 60, height: 240)
.border(.red)
RoundedDiamond()
.frame(width: 120, height: 240)
.border(.red)
RoundedDiamond()
.frame(width: 180, height: 240)
.border(.red)
}
The approach above can be turned into a more generic solution that builds a path from a set of points, using each point as the control point for a quad curve:
struct SimpleRoundedShape: Shape {
let points: [CGPoint]
var cornerRadius: CGFloat = 10
func path(in rect: CGRect) -> Path {
var path = Path()
var finalCurvePoint: CGPoint?
var prevControlPoint: CGPoint? = points.last
for point in points {
if let prevControlPoint {
let dx = point.x - prevControlPoint.x
let dy = point.y - prevControlPoint.y
let len = ((dx * dx) + (dy * dy)).squareRoot()
if len > 0 {
let curveOffset = min(cornerRadius, len / 2)
let xOffset = curveOffset * (dx / len)
let yOffset = curveOffset * (dy / len)
let prevCurveEnd = CGPoint(
x: prevControlPoint.x + xOffset,
y: prevControlPoint.y + yOffset
)
let nextCurveBegin = CGPoint(
x: point.x - xOffset,
y: point.y - yOffset
)
if finalCurvePoint == nil {
finalCurvePoint = prevCurveEnd
path.move(to: nextCurveBegin)
} else {
path.addQuadCurve(to: prevCurveEnd, control: prevControlPoint)
path.addLine(to: nextCurveBegin)
}
}
}
prevControlPoint = point
}
if let finalCurvePoint, let prevControlPoint {
path.addQuadCurve(to: finalCurvePoint, control: prevControlPoint)
}
path.closeSubpath()
return path
}
}
This allows the rounded diamond shape to be simplified to the following:
struct RoundedDiamond: Shape {
var cornerRadius: CGFloat = 10
func path(in rect: CGRect) -> Path {
SimpleRoundedShape(
points: [
CGPoint(x: rect.midX, y: rect.minY),
CGPoint(x: rect.maxX, y: rect.midY),
CGPoint(x: rect.midX, y: rect.maxY),
CGPoint(x: rect.minX, y: rect.midY)
],
cornerRadius: cornerRadius
)
.path(in: rect)
}
}
This produces the same results as before. Let's test it with another "pointy" shape too:
struct PointyShape: Shape {
var cornerRadius: CGFloat = 10
func path(in rect: CGRect) -> Path {
SimpleRoundedShape(
points: [
CGPoint(x: rect.midX, y: rect.midY / 2),
CGPoint(x: rect.maxX, y: rect.minY),
CGPoint(x: rect.maxX * 3 / 4, y: rect.midY),
CGPoint(x: rect.maxX, y: rect.maxY),
CGPoint(x: rect.midX, y: rect.maxY * 3 / 4),
CGPoint(x: rect.minX, y: rect.maxY),
CGPoint(x: rect.midX / 2, y: rect.midY),
CGPoint(x: rect.minX, y: rect.minY)
],
cornerRadius: cornerRadius
)
.path(in: rect)
}
}
PointyShape()
.frame(width: 240, height: 180)
.overlay {
PointyShape(cornerRadius: 8)
.stroke(.white, lineWidth: 3)
.padding(.horizontal, 24)
.padding(.vertical, 18)
}
.overlay {
PointyShape(cornerRadius: 6)
.fill(.yellow)
.padding(.horizontal, 48)
.padding(.vertical, 36)
}
.border(.red)
As you can see, the approach works, but sharp points are not rounded by much and are still quite sharp.
A better approach would be to increase the distance from the control point when the corner is an acute angle, and in fact decrease the distance when it is an obtuse angle.
For a corner which is a right angle, the ideal distance between the start and end points of the curve would be cornerRadius * 2.squareRoot()
. It works quite well to use this as the general case:
struct RoundedShape: Shape {
let points: [CGPoint]
var cornerRadius: CGFloat = 10
func path(in rect: CGRect) -> Path {
var path = Path()
let idealDistanceBetweenCurvePoints = cornerRadius * 2.squareRoot()
let nPoints = points.count
if nPoints > 2 {
var prevPoint = points[nPoints - 1]
var point = points[0]
for index in 0..<nPoints {
let nextPoint = points[index == nPoints - 1 ? 0 : index + 1]
let dxPrev = point.x - prevPoint.x
let dyPrev = point.y - prevPoint.y
let dxNext = point.x - nextPoint.x
let dyNext = point.y - nextPoint.y
let lenPrev = ((dxPrev * dxPrev) + (dyPrev * dyPrev)).squareRoot()
let lenNext = ((dxNext * dxNext) + (dyNext * dyNext)).squareRoot()
if lenPrev > 0 && lenNext > 0 {
let offsetPrev = min(cornerRadius, lenPrev / 2)
let offsetNext = min(cornerRadius, lenNext / 2)
let xOffsetPrev = offsetPrev * (dxPrev / lenPrev)
let yOffsetPrev = offsetPrev * (dyPrev / lenPrev)
let xOffsetNext = offsetNext * (dxNext / lenNext)
let yOffsetNext = offsetNext * (dyNext / lenNext)
let xPrev = point.x - xOffsetPrev
let yPrev = point.y - yOffsetPrev
let xNext = point.x - xOffsetNext
let yNext = point.y - yOffsetNext
let dx = xNext - xPrev
let dy = yNext - yPrev
let distance = ((dx * dx) + (dy * dy)).squareRoot()
if distance > 0 {
let maxScalingFactor = min(
lenPrev / (2 * distance),
lenNext / (2 * distance)
)
let scalingFactor = min(
maxScalingFactor,
idealDistanceBetweenCurvePoints / distance
)
let curveBegin = CGPoint(
x: point.x - (xOffsetPrev * scalingFactor),
y: point.y - (yOffsetPrev * scalingFactor)
)
let curveEnd = CGPoint(
x: point.x - (xOffsetNext * scalingFactor),
y: point.y - (yOffsetNext * scalingFactor)
)
if index == 0 {
path.move(to: curveBegin)
} else {
path.addLine(to: curveBegin)
}
path.addQuadCurve(to: curveEnd, control: point)
} else {
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
}
prevPoint = point
point = nextPoint
}
}
path.closeSubpath()
return path
}
}
When RoundedShape
is now used in place of SimpleRoundedShape
in the diamond shape and pointy shape above, the test cases look like this:
Upvotes: 4