Artefact drawing in Swift

为君一笑 提交于 2019-12-12 13:29:44

问题


The code below draws lines by overriding touches, however there is an artefact that persists when drawing, seen in the images below.

When changing direction while zig zagging drawing across the screen, sometimes the line turns into a flat straight corner instead of remaining circular. The artefact is also experienced when drawing on the spot in small circles, the drawing point flashes half circles sometimes leaving half circles and partial circle residue when the finger leave the screen.

The artefacts are intermittent and not in an entirely consistent or predictable pattern making it difficult to find the issue in the code. It is present both in the simulator and on device in iOS7 - iOS9.

A zip containing two video screen captures of drawing dots and lines along with the Xcode project are uploaded to DropBox in a file called Archive.zip (23MB) https://www.dropbox.com/s/hm39rdiuk0mf578/Archive.zip?dl=0

Questions:

1 - In code, what is causing this dot/half circle artefact and how can it be corrected?

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

回答1:


This would appear to be a fascinating bug in addQuadCurveToPoint and addCurveToPoint where, if the control point(s) are on the same line as the two end points, it doesn't honor the lineJoinStyle. So you can test for this (by looking at the atan2 of the various points and make sure there are not the same), and if so, just do addLineToPoint instead:

I found that this revised code removed those artifacts:

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

Also, this may be overly cautious, but it might be prudent to ensure that two successive points are never the same with a guard statements:

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

If you find other situations where there are problems, you can repeat the debugging exercise that I just did. Namely, run the code until a problem manifested itself, but stop immediately and look at the log of points array to see what points caused a problem, and then create a init?(coder:) that consistently reproduced the problem 100% of the time, e.g.:

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

Then, with a consistently reproducible problem, the debugging was easy. So having diagnosed the problem, I then revised updatePaths until the problem was resolved. I then commented out init? and repeated the whole exercise.



来源:https://stackoverflow.com/questions/35608766/artefact-drawing-in-swift

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!