I'm using the UIBezierPath() and DragGesture in my iOS app to draw lines on a canvas. To achieve this, I collect the gesture points and add them to a path, which is an object of UIBezierPath. I then set a specific lineWidth and stroke the path in a graphics context.
However, I'm facing an issue when trying to draw a circle shape quickly. Instead of getting a smooth curve, the connecting points of the path create corners, resulting in an angular shape rather than a circular one. This issue specifically occurs when drawing circles rapidly, and it affects the overall quality of my drawings. For more visualisation look here:
func draw(size: CGSize, drawingImage: UIImage, lines: [Line], path: UIBezierPath) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
drawingImage.draw(at: .zero)
let pathCount = lines.count
let pointCount = lines[pathCount-1].line.count
let point = lines[pathCount-1].line[pointCount-1]
path.addLine(to: point.toCGPoint())
let context = UIGraphicsGetCurrentContext()!
context.addPath(path.cgPath)
context.setStrokeColor(Color.white.cgColor)
path.lineWidth = 50
path.lineCapStyle = .round
path.lineJoinStyle = .round
path.stroke()
let myImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return myImage
}
I'm seeking guidance on how to ensure that the connecting points of the path create smooth curves even when drawing a circle shape quickly. Any insights or suggestions would be greatly appreciated. Thank you!
you will need to add curves to your bezier rather then lines. and in order for the curves to look good, you will need to calculate the appropriate control points between successive points on the curve.
i'm adapting code from this answer. note that this example accepts an array of CGPoints rather than the class you reference (Line).
execute it like this
let path = quadCurvedPath(points) //points is [CGPoint]
and calculate the UIBezierPath like this
//adapted from https://stackoverflow.com/a/40203583/3680644
func quadCurvedPath(_ points: [CGPoint]) -> UIBezierPath {
let path = UIBezierPath()
var p1 = points[0]
path.move(to: p1)
if (points.count == 2) {
path.addLine(to: points[1])
return path
}
var oldControlP: CGPoint?
for i in 1..<points.count {
let p2 = points[i]
var p3: CGPoint?
if i < points.count - 1 {
p3 = points[i+1]
}
let newControlP = controlPointForPoints(p1: p1, p2: p2, next: p3)
path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2)
p1 = p2
oldControlP = antipodalFor(point: newControlP, center: p2)
}
return path;
}
/// located on the opposite side from the center point
func antipodalFor(point: CGPoint?, center: CGPoint?) -> CGPoint? {
guard let p1 = point, let center = center else {
return nil
}
let newX = 2 * center.x - p1.x
let diffY = abs(p1.y - center.y)
let newY = center.y + diffY * (p1.y < center.y ? 1 : -1)
return CGPoint(x: newX, y: newY)
}
/// halfway of two points
func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
}
/// Find controlPoint2 for addCurve
/// - Parameters:
/// - p1: first point of curve
/// - p2: second point of curve whose control point we are looking for
/// - next: predicted next point which will use antipodal control point for finded
func controlPointForPoints(p1: CGPoint, p2: CGPoint, next p3: CGPoint?) -> CGPoint? {
guard let p3 = p3 else {
return nil
}
let leftMidPoint = midPointForPoints(p1: p1, p2: p2)
let rightMidPoint = midPointForPoints(p1: p2, p2: p3)
var controlPoint = midPointForPoints(p1: leftMidPoint, p2: antipodalFor(point: rightMidPoint, center: p2)!)
if p1.y.between(a: p2.y, b: controlPoint.y) {
controlPoint.y = p1.y
} else if p2.y.between(a: p1.y, b: controlPoint.y) {
controlPoint.y = p2.y
}
let imaginContol = antipodalFor(point: controlPoint, center: p2)!
if p2.y.between(a: p3.y, b: imaginContol.y) {
controlPoint.y = p2.y
}
if p3.y.between(a: p2.y, b: imaginContol.y) {
let diffY = abs(p2.y - p3.y)
controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
}
// make lines easier
controlPoint.x += (p2.x - p1.x) * 0.1
return controlPoint
}
extension CGFloat {
func between(a: CGFloat, b: CGFloat) -> Bool {
return self >= Swift.min(a, b) && self <= Swift.max(a, b)
}
}