Drawing class drawing straight lines instead of curved lines

I have the code below that draws strings using UIBezierPath.

The code uses addCurveToPoint , which should draw curved lines using the cubic bezier path, however, the end result of the code is to draw connected straight lines, but addLineToPoint not used.

What can happen, why not code drawing pattern curves?

enter image description here

 import UIKit class DrawingView: UIView, UITextFieldDelegate { // Modifiable values within the code let lineWidth : CGFloat = 2.0 let lineColor = UIColor.redColor() let lineColorAlpha : CGFloat = 0.4 let shouldAllowUserChangeLineWidth = true let maximumUndoRedoChances = 10 var path = UIBezierPath() var previousImages : [UIImage] = [UIImage]() // Represents current image index var currentImageIndex = 0 // Control points for drawing curve smoothly private var controlPoint1 : CGPoint? private var controlPoint2 : CGPoint? private var undoButton : UIButton! private var redoButton : UIButton! private var textField : UITextField! //MARK: Init methods override init(frame: CGRect) { super.init(frame: frame) setDefaultValues() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setDefaultValues() } // Draw the path when needed override func drawRect(rect: CGRect) { if currentImageIndex > 0 { previousImages[currentImageIndex - 1].drawInRect(rect) } lineColor.setStroke() path.strokeWithBlendMode(CGBlendMode.Normal, alpha: lineColorAlpha) } override func layoutSubviews() { super.layoutSubviews() redoButton.frame = CGRectMake(bounds.size.width - 58, 30, 50, 44) if shouldAllowUserChangeLineWidth { textField.center = CGPointMake(center.x, 52) } } func setDefaultValues() { multipleTouchEnabled = false backgroundColor = UIColor.whiteColor() path.lineWidth = lineWidth addButtonsAndField() } func addButtonsAndField() { undoButton = UIButton(frame: CGRectMake(8, 30, 50, 44)) undoButton.setTitle("Undo", forState: UIControlState.Normal) undoButton.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal) undoButton.backgroundColor = UIColor.lightGrayColor() undoButton.addTarget(self, action: "undoButtonTapped:", forControlEvents: UIControlEvents.TouchUpInside) addSubview(undoButton) redoButton = UIButton(frame: CGRectMake(bounds.size.width - 58, 30, 50, 44)) redoButton.setTitle("Redo", forState: UIControlState.Normal) redoButton.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal) redoButton.backgroundColor = UIColor.lightGrayColor() redoButton.addTarget(self, action: "redoButtonTapped:", forControlEvents: UIControlEvents.TouchUpInside) addSubview(redoButton) if shouldAllowUserChangeLineWidth { textField = UITextField(frame: CGRectMake(0, 0, 50, 40)) textField.backgroundColor = UIColor.lightGrayColor() textField.center = CGPointMake(center.x, 52) textField.keyboardType = UIKeyboardType.NumberPad textField.delegate = self addSubview(textField) } } //MARK: Touches methods override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { // Find the start point and move the path there endEditing(true) let touchPoint = touches.first?.locationInView(self) path.moveToPoint(touchPoint!) } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { let touchPoint = touches.first?.locationInView(self) controlPoint1 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2) controlPoint2 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2) path.addCurveToPoint(touchPoint!, controlPoint1: controlPoint1!, controlPoint2: controlPoint2!) setNeedsDisplay() } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { let touchPoint = touches.first?.locationInView(self) controlPoint1 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2) controlPoint2 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2) path.addCurveToPoint(touchPoint!, controlPoint1: controlPoint1!, controlPoint2: controlPoint2!) savePreviousImage() setNeedsDisplay() // Remove all points to optimize the drawing speed path.removeAllPoints() } override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) { touchesEnded(touches!, withEvent: event) } //MARK: Selector methods func undoButtonTapped(sender : UIButton) { if currentImageIndex > 0 { setNeedsDisplay() currentImageIndex-- } } func redoButtonTapped(sender : UIButton) { if currentImageIndex != previousImages.count { setNeedsDisplay() currentImageIndex++ } } //MARK: UITextFieldDelegate func textFieldDidEndEditing(textField: UITextField) { if let n = NSNumberFormatter().numberFromString(textField.text!) { if n.integerValue > 0 { path.lineWidth = CGFloat(n) } } } //MARK: Saving images for reloading when undo or redo called private func savePreviousImage() { UIGraphicsBeginImageContextWithOptions(bounds.size, true, UIScreen.mainScreen().scale) lineColor.setStroke() // Create a image with white color let rectPath = UIBezierPath(rect: bounds) backgroundColor?.setFill() rectPath.fill() if currentImageIndex > 0 { previousImages[currentImageIndex - 1].drawInRect(bounds) } path.strokeWithBlendMode(CGBlendMode.Normal, alpha: lineColorAlpha) if previousImages.count >= currentImageIndex { previousImages.removeRange(currentImageIndex..<previousImages.count) } if previousImages.count >= maximumUndoRedoChances { previousImages.removeFirst() } else { currentImageIndex++ } previousImages.append(UIGraphicsGetImageFromCurrentImageContext()) UIGraphicsEndImageContext() } } 
0
source share
1 answer

There are several problems:

  • You use control points, which are intermediate points between two points, which leads to line segments. You probably want to select control points that smooth the curve. See http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/ .

    Here is the implementation of the Swift 3 simple smoothing algorithm, as well as the Swift version of the above Hermite and Catmull-Rom splines:

     extension UIBezierPath { /// Simple smoothing algorithm /// /// This iterates through the points in the array, drawing cubic bezier /// from the first to the fourth points, using the second and third as /// control points. /// /// This takes every third point and moves it so that it is exactly inbetween /// the points before and after it, which ensures that there is no discontinuity /// in the first derivative as you join these cubic beziers together. /// /// Note, if, at the end, there are not enough points for a cubic bezier, it /// will perform a quadratic bezier, or if not enough points for that, a line. /// /// - parameter points: The array of `CGPoint`. convenience init?(simpleSmooth points: [CGPoint]) { guard points.count > 1 else { return nil } self.init() move(to: points[0]) var index = 0 while index < (points.count - 1) { switch (points.count - index) { case 2: index += 1 addLine(to: points[index]) case 3: index += 2 addQuadCurve(to: points[index], controlPoint: points[index-1]) case 4: index += 3 addCurve(to: points[index], controlPoint1: points[index-2], controlPoint2: points[index-1]) default: index += 3 let point = CGPoint(x: (points[index-1].x + points[index+1].x) / 2, y: (points[index-1].y + points[index+1].y) / 2) addCurve(to: point, controlPoint1: points[index-2], controlPoint2: points[index-1]) } } } /// Create smooth UIBezierPath using Hermite Spline /// /// This requires at least two points. /// /// Adapted from https://github.com/jnfisher/ios-curve-interpolation /// See http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/ /// /// - parameter hermiteInterpolatedPoints: The array of CGPoint values. /// - parameter closed: Whether the path should be closed or not /// /// - returns: An initialized `UIBezierPath`, or `nil` if an object could not be created for some reason (eg not enough points). convenience init?(hermiteInterpolatedPoints points: [CGPoint], closed: Bool) { self.init() guard points.count > 1 else { return nil } let numberOfCurves = closed ? points.count : points.count - 1 var previousPoint: CGPoint? = closed ? points.last : nil var currentPoint: CGPoint = points[0] var nextPoint: CGPoint? = points[1] move(to: currentPoint) for index in 0 ..< numberOfCurves { let endPt = nextPoint! var mx: CGFloat var my: CGFloat if previousPoint != nil { mx = (nextPoint!.x - currentPoint.x) * 0.5 + (currentPoint.x - previousPoint!.x)*0.5 my = (nextPoint!.y - currentPoint.y) * 0.5 + (currentPoint.y - previousPoint!.y)*0.5 } else { mx = (nextPoint!.x - currentPoint.x) * 0.5 my = (nextPoint!.y - currentPoint.y) * 0.5 } let ctrlPt1 = CGPoint(x: currentPoint.x + mx / 3.0, y: currentPoint.y + my / 3.0) previousPoint = currentPoint currentPoint = nextPoint! let nextIndex = index + 2 if closed { nextPoint = points[nextIndex % points.count] } else { nextPoint = nextIndex < points.count ? points[nextIndex % points.count] : nil } if nextPoint != nil { mx = (nextPoint!.x - currentPoint.x) * 0.5 + (currentPoint.x - previousPoint!.x) * 0.5 my = (nextPoint!.y - currentPoint.y) * 0.5 + (currentPoint.y - previousPoint!.y) * 0.5 } else { mx = (currentPoint.x - previousPoint!.x) * 0.5 my = (currentPoint.y - previousPoint!.y) * 0.5 } let ctrlPt2 = CGPoint(x: currentPoint.x - mx / 3.0, y: currentPoint.y - my / 3.0) addCurve(to: endPt, controlPoint1: ctrlPt1, controlPoint2: ctrlPt2) } if closed { close() } } /// Create smooth UIBezierPath using Catmull-Rom Splines /// /// This requires at least four points. /// /// Adapted from https://github.com/jnfisher/ios-curve-interpolation /// See http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/ /// /// - parameter catmullRomInterpolatedPoints: The array of CGPoint values. /// - parameter closed: Whether the path should be closed or not /// - parameter alpha: The alpha factor to be applied to Catmull-Rom spline. /// /// - returns: An initialized `UIBezierPath`, or `nil` if an object could not be created for some reason (eg not enough points). convenience init?(catmullRomInterpolatedPoints points: [CGPoint], closed: Bool, alpha: Float) { self.init() guard points.count > 3 else { return nil } assert(alpha >= 0 && alpha <= 1.0, "Alpha must be between 0 and 1") let endIndex = closed ? points.count : points.count - 2 let startIndex = closed ? 0 : 1 let kEPSILON: Float = 1.0e-5 move(to: points[startIndex]) for index in startIndex ..< endIndex { let nextIndex = (index + 1) % points.count let nextNextIndex = (nextIndex + 1) % points.count let previousIndex = index < 1 ? points.count - 1 : index - 1 let point0 = points[previousIndex] let point1 = points[index] let point2 = points[nextIndex] let point3 = points[nextNextIndex] let d1 = hypot(Float(point1.x - point0.x), Float(point1.y - point0.y)) let d2 = hypot(Float(point2.x - point1.x), Float(point2.y - point1.y)) let d3 = hypot(Float(point3.x - point2.x), Float(point3.y - point2.y)) let d1a2 = powf(d1, alpha * 2) let d1a = powf(d1, alpha) let d2a2 = powf(d2, alpha * 2) let d2a = powf(d2, alpha) let d3a2 = powf(d3, alpha * 2) let d3a = powf(d3, alpha) var controlPoint1: CGPoint, controlPoint2: CGPoint if fabs(d1) < kEPSILON { controlPoint1 = point2 } else { controlPoint1 = (point2 * d1a2 - point0 * d2a2 + point1 * (2 * d1a2 + 3 * d1a * d2a + d2a2)) / (3 * d1a * (d1a + d2a)) } if fabs(d3) < kEPSILON { controlPoint2 = point2 } else { controlPoint2 = (point1 * d3a2 - point3 * d2a2 + point2 * (2 * d3a2 + 3 * d3a * d2a + d2a2)) / (3 * d3a * (d3a + d2a)) } addCurve(to: point2, controlPoint1: controlPoint1, controlPoint2: controlPoint2) } if closed { close() } } } // Some functions to make the Catmull-Rom splice code a little more readable. // These multiply/divide a `CGPoint` by a scalar and add/subtract one `CGPoint` // from another. private func * (lhs: CGPoint, rhs: Float) -> CGPoint { return CGPoint(x: lhs.x * CGFloat(rhs), y: lhs.y * CGFloat(rhs)) } private func / (lhs: CGPoint, rhs: Float) -> CGPoint { return CGPoint(x: lhs.x / CGFloat(rhs), y: lhs.y / CGFloat(rhs)) } private func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } private func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint { return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) } 

    Here is the “simple” smoothing algorithm, the Hermite spline, and the Catmull Rom spline curves in red, blue, and green, respectively. As you can see, the “simple” smoothing algorithm is calculated simpler, but usually does not go through many of the points (but offers more dramatic smoothing, which eliminates any instability in the beat). Dots bouncing around like this exaggerate the behavior, while in the standard “gesture” it offers a pretty decent smoothing effect. Splines, on the other hand, smooth the curve when passing through points in an array.

    enter image description here

  • If you plan to use iOS 9 and later, it offers several useful features, in particular:

    • Coaxial touches in case the user uses a device capable of such, especially the new iPad. Bottom line, these devices (but not simulators for them) are capable of generating more than 60 strokes per second, and thus, you can get a few touches for each touchesMoved call.

    • Predictable touches when a device can show you where it expects user touches to progress (which will lead to less delay time in your drawing).

    By pulling them together, you can do something like:

     var points: [CGPoint]? var path: UIBezierPath? override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first { points = [touch.location(in: view)] } } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first { if #available(iOS 9.0, *) { if let coalescedTouches = event?.coalescedTouches(for: touch) { points? += coalescedTouches.map { $0.location(in: view) } } else { points?.append(touch.location(in: view)) } if let predictedTouches = event?.predictedTouches(for: touch) { let predictedPoints = predictedTouches.map { $0.location(in: view) } pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points! + predictedPoints, closed: false, alpha: 0.5)?.cgPath } else { pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)?.cgPath } } else { points?.append(touch.location(in: view)) pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)?.cgPath } } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5) pathLayer.path = path?.cgPath } 

    In this code snippet, I am looking at the path, updating CAShapeLayer , but if you want to do it the other way, feel free to. For example, using your drawRect approach, you update path , and then call setNeedsDisplay() .

    And the above shows the syntax if #available(iOS 9, *) { ... } else { ... } if you need to support iOS versions prior to 9.0, but obviously if you only support iOS 9 and later, you You can remove this check and lose the else clause.

    For more information, see the WWDC 2015 Advanced Touch in iOS video.

Anyway, this gives something like:

enter image description here

(For Swift version 2.3 above, see the previous version of this answer.)

+16
source

All Articles