Search code examples
iosswift3core-graphicseraser

In a Swift program, why is my Eraser tool pixelated?


There are numerous questions about creating an eraser tool in CoreGraphics. I cannot find one that matches "pixelated".

Here's the situation. I'm playing with a simple drawing project. The pen tools work fine. The eraser tool is horribly pixelated. Here's a screen shot of what I mean:

enter image description here

Here's the drawing code I'm using:

 //  DrawingView
//  
//
//  Created by David DelMonte on 12/9/16.
//  Copyright © 2016 David DelMonte. All rights reserved.
//


import UIKit


public protocol DrawingViewDelegate {
    func didBeginDrawing(view: DrawingView)
    func isDrawing(view: DrawingView)
    func didFinishDrawing(view: DrawingView)
    func didCancelDrawing(view: DrawingView)
}



open class DrawingView: UIView {
    
    //initial settings
    public var lineColor: UIColor = UIColor.black
    public var lineWidth: CGFloat = 10.0
    public var lineOpacity: CGFloat = 1.0
    //public var lineBlendMode: CGBlendMode = .normal
    
    //used for zoom actions
    public var drawingEnabled: Bool = true
    
    public var delegate: DrawingViewDelegate?
    
    private var currentPoint: CGPoint = CGPoint()
    private var previousPoint: CGPoint = CGPoint()
    private var previousPreviousPoint: CGPoint = CGPoint()
    
    private var pathArray: [Line] = []
    private var redoArray: [Line] = []
    
    var toolType: Int = 0
    
    let π = CGFloat(M_PI)
    private let forceSensitivity: CGFloat = 4.0
    
    
    private struct Line {
        var path: CGMutablePath
        var color: UIColor
        var width: CGFloat
        var opacity: CGFloat
        //var blendMode: CGBlendMode
        
        init(path : CGMutablePath, color: UIColor, width: CGFloat, opacity: CGFloat) {
            self.path = path
            self.color = color
            self.width = width
            self.opacity = opacity
            //self.blendMode = blendMode
        }
    }
    
    override public init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.clear
    }
    
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.backgroundColor = UIColor.clear
    }
    
    override open func draw(_ rect: CGRect) {
        let context : CGContext = UIGraphicsGetCurrentContext()!
        
        for line in pathArray {
            context.setLineWidth(line.width)
            context.setAlpha(line.opacity)
            context.setLineCap(.round)
            
            switch toolType {
            case 0: //pen
                
                context.setStrokeColor(line.color.cgColor)
                context.addPath(line.path)
                context.setBlendMode(.normal)
                
                break
                
            case 1: //eraser
                
                context.setStrokeColor(UIColor.clear.cgColor)
                context.addPath(line.path)
                context.setBlendMode(.clear)
                
                break
                
            case 3: //multiply
                
                context.setStrokeColor(line.color.cgColor)
                context.addPath(line.path)
                context.setBlendMode(.multiply)
                
                break
                
            default:
                break
            }
            
            context.beginTransparencyLayer(auxiliaryInfo: nil)
            context.strokePath()
            context.endTransparencyLayer()
        }
    }
    
    
    
    
    override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard drawingEnabled == true else {
            return
        }
        
        self.delegate?.didBeginDrawing(view: self)
        if let touch = touches.first as UITouch! {
            //setTouchPoints(touch, view: self)
            previousPoint = touch.previousLocation(in: self)
            previousPreviousPoint = touch.previousLocation(in: self)
            currentPoint = touch.location(in: self)
            
            let newLine = Line(path: CGMutablePath(), color: self.lineColor, width: self.lineWidth, opacity: self.lineOpacity)
            newLine.path.addPath(createNewPath())
            pathArray.append(newLine)
        }
    }
    
    override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard drawingEnabled == true else {
            return
        }
        
        self.delegate?.isDrawing(view: self)
        if let touch = touches.first as UITouch! {
            //updateTouchPoints(touch, view: self)
            previousPreviousPoint = previousPoint
            previousPoint = touch.previousLocation(in: self)
            currentPoint = touch.location(in: self)
            
            let newLine = createNewPath()
            if let currentPath = pathArray.last {
                currentPath.path.addPath(newLine)
            }
        }
    }
    
    override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard drawingEnabled == true else {
            return
        }
        
        
    }
    
    override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard drawingEnabled == true else {
            return
        }
        
        
    }
    
    
    
    public func canUndo() -> Bool {
        if pathArray.count > 0 {return true}
        return false
    }
    
    public func canRedo() -> Bool {
        return redoArray.count > 0
    }

    
    public func undo() {
        if pathArray.count > 0 {
            
            redoArray.append(pathArray.last!)
            pathArray.removeLast()
        }
        
        setNeedsDisplay()
    }
    
    public func redo() {
        if redoArray.count > 0 {
            pathArray.append(redoArray.last!)
            redoArray.removeLast()
        }
        setNeedsDisplay()
    }
    
    public func clearCanvas() {
        pathArray = []
        setNeedsDisplay()
    }



    private func createNewPath() -> CGMutablePath {
        //print(#function)
        let midPoints = getMidPoints()
        let subPath = createSubPath(midPoints.0, mid2: midPoints.1)
        let newPath = addSubPathToPath(subPath)
        return newPath
    }
    
    private func calculateMidPoint(_ p1 : CGPoint, p2 : CGPoint) -> CGPoint {
        //print(#function)
        return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5);
    }
    
    private func getMidPoints() -> (CGPoint,  CGPoint) {
        //print(#function)
        let mid1 : CGPoint = calculateMidPoint(previousPoint, p2: previousPreviousPoint)
        let mid2 : CGPoint = calculateMidPoint(currentPoint, p2: previousPoint)
        return (mid1, mid2)
    }
    
    private func createSubPath(_ mid1: CGPoint, mid2: CGPoint) -> CGMutablePath {
        //print(#function)
        let subpath : CGMutablePath = CGMutablePath()
        subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
        subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: previousPoint.x, y: previousPoint.y))
        return subpath
    }
    
    private func addSubPathToPath(_ subpath: CGMutablePath) -> CGMutablePath {
        //print(#function)
        let bounds : CGRect = subpath.boundingBox
        
        let drawBox : CGRect = bounds.insetBy(dx: -0.54 * lineWidth, dy: -0.54 * lineWidth)
        self.setNeedsDisplay(drawBox)
        return subpath
    }
}

UPDATE:

I notice that each eraser touch is square. Please see the second image to show in more detail:

enter image description here

I then rewrote some code as suggested by Pranal Jaiswal:

override open func draw(_ rect: CGRect) {
        print(#function)
        let context : CGContext = UIGraphicsGetCurrentContext()!
        
        if isEraserSelected {
            for line in undoArray {
                //context.beginTransparencyLayer(auxiliaryInfo: nil)
                context.setLineWidth(line.width)
                context.addPath(line.path)
                context.setStrokeColor(UIColor.clear.cgColor)
                context.setBlendMode(.clear)
                context.setAlpha(line.opacity)
                context.setLineCap(.round)
                context.strokePath()

            }
        } else {
            for line in undoArray {
                context.setLineWidth(line.width)
                context.setLineCap(.round)
                context.addPath(line.path)
                context.setStrokeColor(line.color.cgColor)
                context.setBlendMode(.normal)
                context.setAlpha(line.opacity)
                context.strokePath()
            }
            
        }
    }

I'm still getting the same result. What can I try next?


Solution

  • I couldn't exactly look at your code. But I had done something similar in Swift 2.3 a while ago (I do understand you are looking at Swift 3 but right now this is version that I have).

    Here is how the drawing class works looks like.

    import Foundation
    import UIKit
    import QuartzCore
    
    class PRSignatureView: UIView
    
    {
    
    var drawingColor:CGColorRef = UIColor.blackColor().CGColor //Col
    var drawingThickness:CGFloat = 0.5
    var drawingAlpha:CGFloat = 1.0
    
    var isEraserSelected: Bool
    
    private var currentPoint:CGPoint?
    private var previousPoint1:CGPoint?
    private var previousPoint2:CGPoint?
    
    private var path:CGMutablePathRef = CGPathCreateMutable()
    
    var image:UIImage?
    
    required init?(coder aDecoder: NSCoder) {
        //self.backgroundColor = UIColor.clearColor()
        self.isEraserSelected = false
        super.init(coder: aDecoder)
        self.backgroundColor = UIColor.clearColor()
    }
    
    override func drawRect(rect: CGRect)
    {
        self.isEraserSelected ? self.eraseMode() : self.drawingMode()
    }
    
    private func drawingMode()
    {
        if (self.image != nil)
        {
            self.image!.drawInRect(self.bounds)
        }
        let context:CGContextRef = UIGraphicsGetCurrentContext()!
        CGContextAddPath(context, path)
        CGContextSetLineCap(context, CGLineCap.Round)
        CGContextSetLineWidth(context, self.drawingThickness)
        CGContextSetStrokeColorWithColor(context, drawingColor)
        CGContextSetBlendMode(context, CGBlendMode.Normal)
        CGContextSetAlpha(context, self.drawingAlpha)
        CGContextStrokePath(context);
    }
    
    private func eraseMode()
    {
        if (self.image != nil)
        {
            self.image!.drawInRect(self.bounds)
        }
        let context:CGContextRef = UIGraphicsGetCurrentContext()!
        CGContextSaveGState(context)
        CGContextAddPath(context, path);
        CGContextSetLineCap(context, CGLineCap.Round)
        CGContextSetLineWidth(context, self.drawingThickness)
        CGContextSetBlendMode(context, CGBlendMode.Clear)
        CGContextStrokePath(context)
        CGContextRestoreGState(context)
    }
    
    
    
    
    private func midPoint (p1:CGPoint, p2:CGPoint)->CGPoint
    {
        return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5)
    }
    
    private func finishDrawing()
    {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0);
        drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)
        self.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
    
    func clearSignature()
    {
        path = CGPathCreateMutable()
        self.image = nil;
        self.setNeedsDisplay();
    }
    
    // MARK: - Touch Delegates
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        path = CGPathCreateMutable()
        let touch = touches.first!
        previousPoint1 = touch.previousLocationInView(self)
        currentPoint = touch.locationInView(self)
    }
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let touch = touches.first!
        previousPoint2 = previousPoint1
        previousPoint1 = touch.previousLocationInView(self)
        currentPoint = touch.locationInView(self)
        
        let mid1 = midPoint(previousPoint2!, p2: previousPoint1!)
        let mid2 = midPoint(currentPoint!, p2: previousPoint1!)
        
        let subpath:CGMutablePathRef = CGPathCreateMutable()
        CGPathMoveToPoint(subpath, nil, mid1.x, mid1.y)
        CGPathAddQuadCurveToPoint(subpath, nil, previousPoint1!.x, previousPoint1!.y, mid2.x, mid2.y)
        CGPathAddPath(path, nil, subpath);
        self.setNeedsDisplay()
    }
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.touchesMoved(touches, withEvent: event)
        self.finishDrawing()
    }
    override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
        self.touchesMoved(touches!, withEvent: event)
        self.finishDrawing()
    }
    
    }
    

    Source Code for test app I created using the above code

    Edit: Converting few lines code to swift 3 as requested

    subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
    subpath.addQuadCurve(to:CGPoint(x: mid2.x, y: mid2.y) , control: CGPoint(x: previousPoint1!.x, y: previousPoint1!.y))
    path.addPath(subpath)
    

    Edit: In response to the updated Question

    Here is the updated Drawing Class that must solve the issue for sure. https://drive.google.com/file/d/0B5nqEBSJjCriTU5oRXd5c2hRV28/view?usp=sharing&resourcekey=0-8ZE92CSD3j7xxB5jGvgj2w

    Issues addressed:

    1. Line Struct did not hold the tool type associated. Whenever setNeedsDislpay() is called you redraw all the objects in pathArray and all Objects were getting redrawn with the current selected tool. I have added a new variable associatedTool to address the issue.
    2. Use of function beginTransparencyLayer will set the blend mode to kCGBlendModeNormal. As this was common for all cases related to tooltype this was causing the mode to be set to normal. I have removed these two lines

    //context.beginTransparencyLayer(auxiliaryInfo: nil)

    //context.endTransparencyLayer()