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
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
Share Improve this question edited Feb 5 at 4:02 Hadi Fooladi Talari 1,2781 gold badge15 silver badges39 bronze badges asked Feb 4 at 10:27 HeWhoRemainsHeWhoRemains 6111 bronze badges 3- How are the points for the path being collected? Do you add a point for every drag movement, which means there are many (perhaps thousands) of points, mostly very close to each other? Or do you add a single point for every drag release, as it is being done in this answer? For the first approach, you could try using a rolling average to smooth the path. For the second approach, there might be ways of computing better control points for the arcs. – Benzy Neez Commented Feb 4 at 11:54
- let me add whole Code that how I added points – HeWhoRemains Commented Feb 4 at 12:19
- Quick searching turns up many, many resources for "smoothing" a path. The first one I looked at: exploringswift/blog/… appears pretty thorough. – DonMag Commented Feb 4 at 12:28
1 Answer
Reset to default 1The 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()
}
}
}