Smooth Drawing with Apple Pencil - some points out of order
Asked Answered
I

2

2

Following this Answer, I have implemented a CanvasView to draw with the Apple Pencil: https://mcmap.net/q/1771907/-drawing-class-drawing-straight-lines-instead-of-curved-lines

import Foundation
import UIKit

class CanvasView: UIView {

    var points: [CGPoint]?
    var path: UIBezierPath?
    var pathLayer: CAShapeLayer!

    override func layoutSubviews() {

    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        pathLayer = CAShapeLayer()
        pathLayer.fillColor = UIColor.clear.cgColor
        pathLayer.strokeColor = UIColor.red.cgColor
        pathLayer.lineWidth = 3

        self.layer.addSublayer(pathLayer)

        if let touch = touches.first {
            points = [touch.location(in: self)]
        }
    }

    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: self) }
                } else {
                    points?.append(touch.location(in: self))
                }

                if let predictedTouches = event?.predictedTouches(for: touch) {
                    let predictedPoints = predictedTouches.map { $0.location(in: self) }
                    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: self))
                pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)?.cgPath
            }
        }


    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)?.cgPath
        points?.removeAll()
    }
}

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

Unfortunately, it is not that smooth because some points are out of the line which you can see here: enter image description here

I've tried to turn off the predicted touches, but it doesn't help much. What else can I do to optimize this?

Ianthe answered 1/4, 2018 at 9:18 Comment(3)
I tried your code and it works perfectly on my side. How can I reproduce it?Frostbite
Do they always appear in the same spots?Stichomythia
iPhone smooth sketch drawing algorithm https://mcmap.net/q/146158/-iphone-smooth-sketch-drawing-algorithm maybe helpfulKimmy
I
2

I just checked your CanvasView Code. And applied changes in the code. Now code look like this:

import Foundation
import UIKit

class CanvasView: UIView {

var points: [CGPoint]?
var path: UIBezierPath?
var pathLayer: CAShapeLayer!

override func layoutSubviews() {

}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    pathLayer = CAShapeLayer()
    pathLayer.fillColor = UIColor.clear.cgColor
    pathLayer.strokeColor = UIColor.red.cgColor
    pathLayer.lineWidth = 1
    pathLayer.lineJoin = kCALineJoinRound
    pathLayer.lineCap = kCALineCapRound
    self.layer.addSublayer(pathLayer)

    if let touch = touches.first {

        points = [touch.location(in: self)]
    }
}

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: self) }
            }
            else {

                points?.append(touch.location(in: self))
            }

            if let predictedTouches = event?.predictedTouches(for: touch) {

                let predictedPoints = predictedTouches.map { $0.location(in: self) }
                pathLayer.path = UIBezierPath.interpolateHermiteFor(points: points! + predictedPoints, closed: false).cgPath
            }
            else {

                pathLayer.path = UIBezierPath.interpolateHermiteFor(points: points!, closed: false).cgPath
            }
        }
        else {

            points?.append(touch.location(in: self))
            pathLayer.path = UIBezierPath.interpolateHermiteFor(points: points!, closed: false).cgPath
        }
    }
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

    pathLayer.path = UIBezierPath.interpolateHermiteFor(points: points!, closed: false).cgPath
    points?.removeAll()
}
}


extension UIBezierPath {

static func interpolateHermiteFor(points: [CGPoint], closed: Bool = false) -> UIBezierPath {
    guard points.count >= 2 else {
        return UIBezierPath()
    }

    if points.count == 2 {
        let bezierPath = UIBezierPath()
        bezierPath.move(to: points[0])
        bezierPath.addLine(to: points[1])
        return bezierPath
    }

    let nCurves = closed ? points.count : points.count - 1

    let path = UIBezierPath()
    for i in 0..<nCurves {
        var curPt = points[i]
        var prevPt: CGPoint, nextPt: CGPoint, endPt: CGPoint
        if i == 0 {
            path.move(to: curPt)
        }

        var nexti = (i+1)%points.count
        var previ = (i-1 < 0 ? points.count-1 : i-1)

        prevPt = points[previ]
        nextPt = points[nexti]
        endPt = nextPt

        var mx: CGFloat
        var my: CGFloat
        if closed || i > 0 {
            mx  = (nextPt.x - curPt.x) * CGFloat(0.5)
            mx += (curPt.x - prevPt.x) * CGFloat(0.5)
            my  = (nextPt.y - curPt.y) * CGFloat(0.5)
            my += (curPt.y - prevPt.y) * CGFloat(0.5)
        }
        else {
            mx = (nextPt.x - curPt.x) * CGFloat(0.5)
            my = (nextPt.y - curPt.y) * CGFloat(0.5)
        }

        var ctrlPt1 = CGPoint.zero
        ctrlPt1.x = curPt.x + mx / CGFloat(3.0)
        ctrlPt1.y = curPt.y + my / CGFloat(3.0)

        curPt = points[nexti]

        nexti = (nexti + 1) % points.count
        previ = i;

        prevPt = points[previ]
        nextPt = points[nexti]

        if closed || i < nCurves-1 {
            mx  = (nextPt.x - curPt.x) * CGFloat(0.5)
            mx += (curPt.x - prevPt.x) * CGFloat(0.5)
            my  = (nextPt.y - curPt.y) * CGFloat(0.5)
            my += (curPt.y - prevPt.y) * CGFloat(0.5)
        }
        else {
            mx = (curPt.x - prevPt.x) * CGFloat(0.5)
            my = (curPt.y - prevPt.y) * CGFloat(0.5)
        }

        var ctrlPt2 = CGPoint.zero
        ctrlPt2.x = curPt.x - mx / CGFloat(3.0)
        ctrlPt2.y = curPt.y - my / CGFloat(3.0)

        path.addCurve(to: endPt, controlPoint1:ctrlPt1, controlPoint2:ctrlPt2)
    }

    if closed {
        path.close()
    }

    return path
}
}

I just used Hermite interpolate instead of your catmullRom interpolate and able to get smooth drawing in canvas. Well I don't have apple pencil to check this code. So you please tell me is it working fine or not.

enter image description here enter image description here

Inconsequential answered 14/4, 2018 at 16:3 Comment(1)
I implemented above mentioned code. It is showing a straight line from first point to current point with black back ground. Except of this it is perfect.Settles
J
2

You can try this for better smoothness.

/** To get mid point

- parameter firstPoint  
- parameter secondPont 

*/

CGPoint midPoint(CGPoint firstPoint, CGPoint secondPont)
{

    return CGPointMake((firstPoint.x + secondPont.x) * 0.5, (firstPoint.y + secondPont.y) * 0.5);

}
/** 
 touch began method
*/
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{

    UITouch *touch = [touches anyObject];

    previousPoint1 = [touch previousLocationInView:self];
    previousPoint2 = [touch previousLocationInView:self];
    currentPoint = [touch locationInView:self];

}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{

    UITouch *touch = [touches anyObject];

    previousPoint2 = previousPoint1;
    previousPoint1 = [touch previousLocationInView:self];
    currentPoint = [touch locationInView:self];


    // calculate mid point
    CGPoint mid1 = midPoint(previousPoint1, previousPoint2); 
    CGPoint mid2 = midPoint(currentPoint, previousPoint1);

    UIGraphicsBeginImageContext(self.imageView.frame.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [self.imageView.image drawInRect:CGRectMake(0, 0, self.imageView.frame.size.width, self.imageView.frame.size.height)];

    CGContextMoveToPoint(context, mid1.x, mid1.y);
    // Use QuadCurve is the key
    CGContextAddQuadCurveToPoint(context, previousPoint1.x, previousPoint1.y, mid2.x, mid2.y); 

    CGContextSetLineCap(context, kCGLineCapRound);
    CGContextSetLineWidth(context, 2.0);
    CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
    CGContextStrokePath(context);

    self.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

}

In Swift, You can try this one:

import UIKit

class drawImageView: UIImageView {

    var previousPoint1 = CGPoint()

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        previousPoint1 = touch.previousLocation(in: self)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        let previousPoint2 = previousPoint1
        previousPoint1 = touch.previousLocation(in: self)
        let currentPoint = touch.location(in: self)


        // calculate mid point
        let mid1 = midPoint(p1: previousPoint1, p2: previousPoint2)
        let mid2 = midPoint(p1: currentPoint, p2: previousPoint1)

        UIGraphicsBeginImageContext(self.frame.size)
        guard let context = UIGraphicsGetCurrentContext() else { return }
        if let image = self.image {
            image.draw(in: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
        }

        context.move(to: mid1)
        context.addQuadCurve(to: mid2, control: previousPoint1)

        context.setLineCap(.round)
        context.setLineWidth(2.0)
        context.setStrokeColor(red: 1.0, green: 0, blue: 0, alpha: 1.0)
        context.strokePath()

        self.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }

    func midPoint(firstPoint: CGPoint, secondPoint: CGPoint) -> CGPoint {
        return CGPoint(x: (firstPoint.x + secondPoint.x) / 2.0, y: (firstPoint.y + secondPoint.y) / 2.0)
    }
}
Joleen answered 14/4, 2018 at 19:1 Comment(1)
Simple and smooth!Instantaneity

© 2022 - 2024 — McMap. All rights reserved.