Iphone Smooth Sketch Drawing Algorithm

iPhone smooth sketch drawing algorithm

The easiest way to smooth a curve like this is to use a Bezier curve instead of straight line segments. For the math behind this, see this article (pointed to in this answer), which describes how to calculate the curves required to smooth a curve that passes through multiple points.

I believe that the Core Plot framework now has the ability to smooth the curves of plots, so you could look at the code used there to implement this kind of smoothing.

There's no magic to any of this, as these smoothing routines are fast and relatively easy to implement.

Smooth Drawing with Apple Pencil - some points out of order

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, 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, 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, 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.. 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.

Sample Image
Sample Image

Example code on how to achieve smooth paint strokes like those seen in the Penultimate app

This has been discussed a few times. You need to use bezair curves and OpenGL. I don't want to rewrite it all, so here is a link to an answer I posted a while ago on the same topic. It is a rather robust answer and should help you get in the right direction.

How to smooth marker stroke for iPhone?

I achieved this smoothness with the help of this code https://github.com/levinunnink/Smooth-Line-View

UIBezierPath onTouch Drawing Example with smooth draw line

I have one example for free hand drawing but one problem is it is not much smoother but you can have one starting point using this example.

Happy Coding :)

Smoothing a hand-drawn free shape

You could use some kind of curve fitting (maybe Bezier curve) to do it for you.

There is also this very nice example of how it could work. I could not find source code for it but i think that creator of this one has used algorithm from Graphics Gems 1. You could find c code for it here and i have found on SO also this.

How to Undo & Redo a smooth line in iPhone?

Use the NSUndoManager. However, if you are painting lines on the canvas, you will also need to keep their representation around as well (so you can pop them off).

So, whether you collect them as a UIBezierPath, or use shape layers, or your own "array of points" you undo in the same manner.

So, while drawing the line, keep a record of the points you used in your drawing. When the drawing is done (e.g., touchesEnded), you want to "push" your drawing, and tell the undo manager how to undo it. Simply, it would be something like this almost-code...

- (void)pushDrawing:(Drawing*)drawing
{
[self.stack push:drawing];
[self.undoManager registerUndoWithTarget: self
selector: @selector(popDrawing)
object: nil];
}

- (void)popDrawing:(Drawing*)drawing
{
Drawing *drawing = [self.stack pop];
[self.undoManager registerUndoWithTarget: self
selector: @selector(pushDrawing:)
object: drawing];
}

If you are using one canvas, you may have to redraw the entire thing, especially when popping a drawing off. If you are using views or layers, you may not...

Look at the docs for NSUndoManager... it's available on iOS, and has good examples. It "remembers" if you you undo-ing or redo-ing, and will do the right thing, so the above could be implemented as one function (but it's easier to understand at first with one function going each direction).



Related Topics



Leave a reply



Submit