Swift 中的人工制品绘图
Artefact drawing in Swift
下面的代码通过覆盖触摸来绘制线条,但是在绘制时存在一个伪像,如下图所示。
当在屏幕上锯齿状绘制时改变方向,有时线条会变成平坦的直角而不是保持圆形。原地画小圆圈也有这个现象,画点闪动半圈,手指离开屏幕时有时会留下半圈和部分圆圈残留。
人工制品是断断续续的,不是完全一致或可预测的模式,因此很难在代码中找到问题。它存在于模拟器和设备上 iOS7 - iOS9.
一个包含两个绘图点和线的视频屏幕截图的 zip 以及 Xcode 项目被上传到 DropBox 的文件中名为 Archive.zip (23MB) https://www.dropbox.com/s/hm39rdiuk0mf578/Archive.zip?dl=0
问题:
1 - 在代码中,是什么导致了这个 dot/half 圆形伪影,如何纠正?
class SmoothCurvedLinesView: UIView {
var strokeColor = UIColor.blueColor()
var lineWidth: CGFloat = 20
var snapshotImage: UIImage?
private var path: UIBezierPath?
private var temporaryPath: UIBezierPath?
private var points = [CGPoint]()
private var totalPointCount = 0
override func drawRect(rect: CGRect) {
snapshotImage?.drawInRect(rect)
strokeColor.setStroke()
path?.stroke()
temporaryPath?.stroke()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
points = [touch!.locationInView(self)]
totalPointCount = totalPointCount + 1
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
points.append(point)
totalPointCount = totalPointCount + 1
updatePaths()
if totalPointCount > 50 {
constructIncrementalImage(includeTemporaryPath: false)
path = nil
totalPointCount = 0
}
setNeedsDisplay()
}
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStartingAtPoint(points[0])
}
path?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
points.removeFirst(3)
}
// build temporary path up to last touch point
let pointCount = points.count
if pointCount == 2 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addLineToPoint(points[1])
} else if pointCount == 3 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
} else if pointCount == 4 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
constructIncrementalImage()
path = nil
setNeedsDisplay()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
touchesEnded(touches!, withEvent: event)
}
private func createPathStartingAtPoint(point: CGPoint) -> UIBezierPath {
let localPath = UIBezierPath()
localPath.moveToPoint(point)
localPath.lineWidth = lineWidth
localPath.lineCapStyle = .Round
localPath.lineJoinStyle = .Round
return localPath
}
private func constructIncrementalImage(includeTemporaryPath includeTemporaryPath: Bool = true) {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
strokeColor.setStroke()
snapshotImage?.drawAtPoint(CGPointZero)
path?.stroke()
if (includeTemporaryPath) { temporaryPath?.stroke() }
snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
这似乎是 addQuadCurveToPoint
和 addCurveToPoint
中的一个令人着迷的错误,其中,如果控制点与两个端点在同一条线上,它不会接受lineJoinStyle
。所以你可以对此进行测试(通过查看各个点的 atan2
并确保它们不相同),如果是这样,只需执行 addLineToPoint
即可:
我发现这个修改后的代码删除了那些工件:
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStartingAtPoint(points[0])
}
addCubicCurveToPath(path)
points.removeFirst(3)
}
// build temporary path up to last touch point
let pointCount = points.count
if pointCount == 2 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addLineToPoint(points[1])
} else if pointCount == 3 {
temporaryPath = createPathStartingAtPoint(points[0])
addQuadCurveToPath(temporaryPath)
} else if pointCount == 4 {
temporaryPath = createPathStartingAtPoint(points[0])
addCubicCurveToPath(temporaryPath)
}
}
/// Add cubic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than cubic bezier path.
private func addCubicCurveToPath(somePath: UIBezierPath?) {
let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
let m23 = atan2(points[2].x - points[3].x, points[2].y - points[3].y)
let m03 = atan2(points[0].x - points[3].x, points[0].y - points[3].y)
if m01 == m03 || m23 == m03 || points[0] == points[3] {
somePath?.addLineToPoint(points[1])
somePath?.addLineToPoint(points[2])
somePath?.addLineToPoint(points[3])
} else {
somePath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
/// Add quadratic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than quadratic bezier path.
private func addQuadCurveToPath(somePath: UIBezierPath?) {
let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
let m12 = atan2(points[1].x - points[2].x, points[1].y - points[2].y)
let m02 = atan2(points[0].x - points[2].x, points[0].y - points[2].y)
if m01 == m02 || m12 == m02 || points[0] == points[2] {
somePath?.addLineToPoint(points[1])
somePath?.addLineToPoint(points[2])
} else {
somePath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
}
}
此外,这可能过于谨慎,但使用 guard
语句确保两个连续的点永远不相同可能是谨慎的做法:
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
guard point != points.last else { return }
points.append(point)
totalPointCount = totalPointCount + 1
updatePaths()
if totalPointCount > 50 {
constructIncrementalImage(includeTemporaryPath: false)
path = nil
totalPointCount = 0
}
setNeedsDisplay()
}
如果你发现其他情况有问题,你可以重复我刚才的调试练习。即,运行 代码直到出现问题,但立即停止并查看 points
数组的日志以查看哪些点导致了问题,然后创建一个 init?(coder:)
一致重现100% 的时间问题,例如:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
points.append(CGPoint(x: 239.33332824707, y: 419.0))
points.append(CGPoint(x: 239.33332824707, y: 420.0))
points.append(CGPoint(x: 239.33332824707, y: 419.3))
updatePaths()
}
然后,有了一个始终可重现的问题,调试就很容易了。因此,诊断出问题后,我修改了updatePaths
,直到问题得到解决。然后我注释掉 init?
并重复了整个练习。
下面的代码通过覆盖触摸来绘制线条,但是在绘制时存在一个伪像,如下图所示。
当在屏幕上锯齿状绘制时改变方向,有时线条会变成平坦的直角而不是保持圆形。原地画小圆圈也有这个现象,画点闪动半圈,手指离开屏幕时有时会留下半圈和部分圆圈残留。
人工制品是断断续续的,不是完全一致或可预测的模式,因此很难在代码中找到问题。它存在于模拟器和设备上 iOS7 - iOS9.
一个包含两个绘图点和线的视频屏幕截图的 zip 以及 Xcode 项目被上传到 DropBox 的文件中名为 Archive.zip (23MB) https://www.dropbox.com/s/hm39rdiuk0mf578/Archive.zip?dl=0
问题:
1 - 在代码中,是什么导致了这个 dot/half 圆形伪影,如何纠正?
class SmoothCurvedLinesView: UIView {
var strokeColor = UIColor.blueColor()
var lineWidth: CGFloat = 20
var snapshotImage: UIImage?
private var path: UIBezierPath?
private var temporaryPath: UIBezierPath?
private var points = [CGPoint]()
private var totalPointCount = 0
override func drawRect(rect: CGRect) {
snapshotImage?.drawInRect(rect)
strokeColor.setStroke()
path?.stroke()
temporaryPath?.stroke()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
points = [touch!.locationInView(self)]
totalPointCount = totalPointCount + 1
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
points.append(point)
totalPointCount = totalPointCount + 1
updatePaths()
if totalPointCount > 50 {
constructIncrementalImage(includeTemporaryPath: false)
path = nil
totalPointCount = 0
}
setNeedsDisplay()
}
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStartingAtPoint(points[0])
}
path?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
points.removeFirst(3)
}
// build temporary path up to last touch point
let pointCount = points.count
if pointCount == 2 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addLineToPoint(points[1])
} else if pointCount == 3 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
} else if pointCount == 4 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
constructIncrementalImage()
path = nil
setNeedsDisplay()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
touchesEnded(touches!, withEvent: event)
}
private func createPathStartingAtPoint(point: CGPoint) -> UIBezierPath {
let localPath = UIBezierPath()
localPath.moveToPoint(point)
localPath.lineWidth = lineWidth
localPath.lineCapStyle = .Round
localPath.lineJoinStyle = .Round
return localPath
}
private func constructIncrementalImage(includeTemporaryPath includeTemporaryPath: Bool = true) {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
strokeColor.setStroke()
snapshotImage?.drawAtPoint(CGPointZero)
path?.stroke()
if (includeTemporaryPath) { temporaryPath?.stroke() }
snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
这似乎是 addQuadCurveToPoint
和 addCurveToPoint
中的一个令人着迷的错误,其中,如果控制点与两个端点在同一条线上,它不会接受lineJoinStyle
。所以你可以对此进行测试(通过查看各个点的 atan2
并确保它们不相同),如果是这样,只需执行 addLineToPoint
即可:
我发现这个修改后的代码删除了那些工件:
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStartingAtPoint(points[0])
}
addCubicCurveToPath(path)
points.removeFirst(3)
}
// build temporary path up to last touch point
let pointCount = points.count
if pointCount == 2 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addLineToPoint(points[1])
} else if pointCount == 3 {
temporaryPath = createPathStartingAtPoint(points[0])
addQuadCurveToPath(temporaryPath)
} else if pointCount == 4 {
temporaryPath = createPathStartingAtPoint(points[0])
addCubicCurveToPath(temporaryPath)
}
}
/// Add cubic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than cubic bezier path.
private func addCubicCurveToPath(somePath: UIBezierPath?) {
let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
let m23 = atan2(points[2].x - points[3].x, points[2].y - points[3].y)
let m03 = atan2(points[0].x - points[3].x, points[0].y - points[3].y)
if m01 == m03 || m23 == m03 || points[0] == points[3] {
somePath?.addLineToPoint(points[1])
somePath?.addLineToPoint(points[2])
somePath?.addLineToPoint(points[3])
} else {
somePath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
/// Add quadratic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than quadratic bezier path.
private func addQuadCurveToPath(somePath: UIBezierPath?) {
let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
let m12 = atan2(points[1].x - points[2].x, points[1].y - points[2].y)
let m02 = atan2(points[0].x - points[2].x, points[0].y - points[2].y)
if m01 == m02 || m12 == m02 || points[0] == points[2] {
somePath?.addLineToPoint(points[1])
somePath?.addLineToPoint(points[2])
} else {
somePath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
}
}
此外,这可能过于谨慎,但使用 guard
语句确保两个连续的点永远不相同可能是谨慎的做法:
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
guard point != points.last else { return }
points.append(point)
totalPointCount = totalPointCount + 1
updatePaths()
if totalPointCount > 50 {
constructIncrementalImage(includeTemporaryPath: false)
path = nil
totalPointCount = 0
}
setNeedsDisplay()
}
如果你发现其他情况有问题,你可以重复我刚才的调试练习。即,运行 代码直到出现问题,但立即停止并查看 points
数组的日志以查看哪些点导致了问题,然后创建一个 init?(coder:)
一致重现100% 的时间问题,例如:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
points.append(CGPoint(x: 239.33332824707, y: 419.0))
points.append(CGPoint(x: 239.33332824707, y: 420.0))
points.append(CGPoint(x: 239.33332824707, y: 419.3))
updatePaths()
}
然后,有了一个始终可重现的问题,调试就很容易了。因此,诊断出问题后,我修改了updatePaths
,直到问题得到解决。然后我注释掉 init?
并重复了整个练习。