Dessiner des lignes droites au lieu de lignes courbes

J'ai le code ci-dessous qui dessine des lignes en utilisant UIBezierPath.

Le code utilise addCurveToPoint qui devrait dessiner des lignes courbes en utilisant un path bezier cubique, cependant le résultat final du code dessine à la place des lignes droites connectées mais addLineToPoint n'est pas utilisé.

Qu'est-ce qui pourrait se passer, pourquoi le code ne dessine-t-il pas des lignes courbes?

entrez la description de l'image ici

 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().numberFromSsortingng(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() } } 

Il y a quelques problèmes:

  1. Vous utilisez des points de contrôle qui sont des points intermédiaires entre les deux points, résultant en segments de ligne. Vous voulez probablement choisir des points de contrôle qui lissent la courbe. Voir http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/ .

    Voici une implémentation Swift 3 d'un algorithm de lissage simple, ainsi que des rendus Swift des approches Hermite et Catmull-Rom Spline ci-dessus:

     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) } 

    Voici l'algorithm de lissage "simple", la spline "Hermite" et les courbes splines "Catmull Rom" en rouge, bleu et vert, respectivement. Comme vous pouvez le voir, l'algorithm de lissage "simple" est plus simple d'un sharepoint vue informatique, mais ne passe généralement pas par un grand nombre de points (mais offre un lissage plus spectaculaire qui élimine toute instabilité dans le contour). Les points qui sautillent comme ça exagèrent le comportement, alors que dans un "geste" standard, il offre un effet de lissage assez décent. Les splines, d'autre part lisser la courbe en passant par les points dans le tableau.

    entrez la description de l'image ici

  2. Si vous ciblez iOS 9 et les versions ultérieures, il présente quelques fonctionnalités intéressantes, notamment:

    • Touches coalesced dans le cas où l'user utilise un appareil capable de tels, notamment les nouveaux iPads. Bottom line, ces appareils (mais pas les simulateurs pour eux) sont capables de générer plus de 60 touches par seconde, et ainsi vous pouvez get plusieurs touches signalées pour chaque appel à touchesMoved .

    • Les touches prédites, où l'appareil peut vous montrer où il anticipe les touches de l'user, progressent (ce qui réduit le time de latence dans votre dessin).

    En les rassemblant, vous pourriez faire quelque chose comme:

     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 } 

    Dans cet extrait de code, je rends le path en mettant à jour un CAShapeLayer , mais si vous voulez le rendre autrement, n'hésitez pas. Par exemple, en utilisant votre approche drawRect , vous devez mettre à jour le path , puis appeler setNeedsDisplay() .

    Et, ce qui précède illustre la if #available(iOS 9, *) { ... } else { ... } si vous avez besoin de supporter les versions iOS antérieures à 9.0, mais évidemment, si vous ne supportez que iOS 9 et plus tard , vous pouvez supprimer cette vérification et perdre la clause else .

    Pour plus d'informations, voir la video WWDC 2015 Advanced Touch Input sur iOS .

Quoi qu'il en soit, cela donne quelque chose comme:

entrez la description de l'image ici

(Pour Swift 2.3 interprétation de ce qui précède, s'il vous plaît voir la version précédente de cette réponse.)