I am working on a drawing app in SwiftUI where users can draw freeform lines. I want to smooth the path when it's drawn so that sharp turns are not visible and the path looks smoother. Currently, I’m using a quadratic Bézier curve to draw the path, but it still has sharp corners when users change direction abruptly.
Here is my current code for drawing the path:
@State private var dragPath: [CGPoint] = []
ZStack{
Path { path in
guard dragPath.count >= 2 else { return }
path.move(to: dragPath[0])
for i in 1..<dragPath.count {
let previousPoint = dragPath[i - 1]
let currentPoint = dragPath[i]
let controlPoint = CGPoint(
x: (previousPoint.x + currentPoint.x) / 2,
y: (previousPoint.y + currentPoint.y) / 2
)
path.addQuadCurve(to: currentPoint, control: controlPoint)
}
}
.stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
}.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
if !isCreated {
dragPath.append(value.location)
}
}
.onEnded { _ in isCreated = true }
)
Is there a way to further smooth out the path to avoid any sharp turns when the user changes direction? Ideally, I want the path to appear continuous and fluid, even if the user draws in an erratic manner.
Additionally, I have attached a screenshot of the desired outcome for your reference.
this is what I want
this is the result what I got right now
The way you are collecting the drag points is to add a new point for every drag movement. This will give a path with many (perhaps thousands) of points, mostly very close to each other.
I would suggest using a rolling average to smooth these points.
In your original screenshot, you actually have two paths. The smaller path (the point of the arrow) probably has only a few points. For smaller paths, you probably don't want to apply so much smoothing.
Here is an updated version of your example to show the path being smoothed in this way:
struct ContentView: View {
private typealias Points = [CGPoint]
@State private var allPaths = [Points]()
@State private var dragPath = Points()
let maxOffsetForAverage = 5
private func drawPath(points: Points) -> some View {
Path { path in
let nPoints = points.count
let maxOffset = max(1, min(maxOffsetForAverage, nPoints / maxOffsetForAverage))
var xSum = CGFloat.zero
var ySum = CGFloat.zero
var previousRangeBegin = 0
var previousRangeEnd = 0
for i in 0..<nPoints {
let rangeBegin = max(0, i - maxOffset)
let rangeEnd = min(nPoints - 1, i + maxOffset)
if i == 0, let firstPoint = points.first {
path.move(to: firstPoint)
for point in points[rangeBegin...rangeEnd] {
xSum += point.x
ySum += point.y
}
} else {
if rangeBegin > previousRangeBegin {
let previousPoint = points[previousRangeBegin]
xSum -= previousPoint.x
ySum -= previousPoint.y
}
if rangeEnd > previousRangeEnd {
let endPoint = points[rangeEnd]
xSum += endPoint.x
ySum += endPoint.y
}
let sampleSize = CGFloat(rangeEnd - rangeBegin + 1)
let point = CGPoint(x: xSum / sampleSize, y: ySum / sampleSize)
path.addLine(to: point)
}
previousRangeBegin = rangeBegin
previousRangeEnd = rangeEnd
}
if nPoints > 2, let lastPoint = points.last {
path.addLine(to: lastPoint)
}
}
.stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
}
var body: some View {
VStack {
ZStack {
Image(systemName: "ladybug")
.resizable()
.scaledToFit()
ForEach(Array(allPaths.enumerated()), id: \.offset) { offset, points in
drawPath(points: points)
}
if !dragPath.isEmpty {
drawPath(points: dragPath)
}
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { val in
dragPath.append(val.location)
}
.onEnded { val in
var currentPath = dragPath
currentPath.append(val.location)
allPaths.append(currentPath)
dragPath.removeAll()
}
)
.frame(width: 300, height: 300)
.border(.gray)
Button("Reset") {
allPaths.removeAll()
}
.buttonStyle(.bordered)
.padding()
}
}
}