使用Apple Pencil平滑绘图-某些点出故障 [英] Smooth Drawing with Apple Pencil - some points out of order
问题描述
在此答案之后,我实现了一个CanvasView以Apple Pencil进行绘制:
我试图关闭预测的触摸,但是并没有太大帮助。我还可以做些什么来优化此效果?
我刚刚检查了您的CanvasView代码。并在代码中应用更改。现在代码如下所示:
import Foundation
import UIKit
class CanvasView:UIView {
var分:[CGPoint]?
var路径:UIBezierPath?
var pathLayer:CAShapeLayer!
覆盖func layoutSubviews(){
}
覆盖func touchesBegan(_ touches:Set< UITouch> ;,事件: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)
如果让我们触摸= touches.first {
points = [touch.location(in:self)]
}
}
覆盖func touchesMoved(_ touches:Set< UITouch> ;,事件:UIEvent?) {
如果让touch = touches.first {
如果#available(iOS 9.0,*){
如果让coalescedTouches = event?。 coalescedTouches(for:touch){
分? + = coalescedTouches.map {$ 0.location(in:self)}
}
else {
points?.append(touch.location(in:self))
}
如果让预测触摸=事件?.predictedTouches(用于:触摸){
让预测点=预测触摸.map {$ 0.location(在:自我中)}
pathLayer.path = UIBezierPath.interpolateHermiteFor(要点:!! (points:points !, close:false).cgPath
}
}
else {
points?.append(touch.location(in:self))
pathLayer.path = UIBezierPath.interpolateHermiteFor(points:points !, close:false).cgPath
}
}
}
覆盖函数touchesEnded( _触摸:设置< UITouch> ;,并带有事件:UIEven t?){
pathLayer.path = UIBezierPath.interpolateHermiteFor(points:points !, close:false).cgPath
points?.removeAll()
}
}
扩展UIBezierPath {
static func interpolateHermiteFor(points:[CGPoint],close:Bool = false)-> UIBezierPath {
guard points.count> = 2 else {
return UIBezierPath()
}
if points.count == 2 {
让bezierPath = UIBezierPath()
bezierPath.move(to:points [0])
bezierPath.addLine(to:points [1])$ b $ b return bezierPath
}
让nCurves =已关闭? points.count:points.count-1
让路径= UIBezierPath()
for i in 0 ..< nCurves {
var curPt = points [i]
var prevPt:CGPoint,nextPt:CGPoint,endPt:CGPoint
如果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
如果关闭||我> 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 =点[previ]
nextPt =点[nexti]
如果关闭||我< 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-我的/ CGFloat(3.0)
path.addCurve(to:endPt,controlPoint1:ctrlPt1,controlPoint2:ctrlPt2)
}
如果关闭{
path.close()
}
返回路径
}
}
我只是使用Hermite插值法而不是您的catmullRom进行插值,并能够在画布上获得平滑的绘图。好吧,我没有苹果铅笔来检查此代码。所以,请您告诉我它是否工作正常。
Following this Answer, I have implemented a CanvasView to draw with the Apple Pencil: https://stackoverflow.com/a/34583708/2529173
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:
I've tried to turn off the predicted touches, but it doesn't help much. What else can I do to optimize this?
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.
这篇关于使用Apple Pencil平滑绘图-某些点出故障的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!