1

我一直在到处寻找一个被问了很多次的问题的答案。我花了几个小时浏览 SO 和 Google。必须有一个不费吹灰之力的答案。

我正在开发一个矢量绘图应用程序,最终使绘图和撤消功能正常工作。现在我需要一个橡皮擦:-o

编辑:根据@DonMag 的精彩文章,我能够非常接近橡皮擦,但有些东西仍然不太正确。因此,我将尝试解释我的视图和图层在应用程序中的位置以及我这样做的原因:

从底部视图/层到顶部...

  1. BackgroundImageView - 我正在使用这个图像视图来保存绘图表面的“背景”。这是一个可以更改的层,可以保存和调用新的“模板”。我将它分开,以便用户无法擦除绘图表面。背景由 CAShapeLayers 组成,它们被绘制来代表不同的纸张类型。

  2. MainImageView - 我正在使用这个图像视图来完成用户启动的所有绘图。所以我触摸并拖动手指,新的 CAShapeLayer 被添加到图像视图中。这使用户的绘图与“绘图表面”分开。这也是我希望擦除发生的地方

  3. PageImagesView - 我使用这个视图来保存用户可以添加到页面的图像,并移动/调整它们的大小。我不希望橡皮擦影响图像,但是如果在 MainImageView 中绘制的一条线穿过图像并需要擦除,它应该让图像显示出来,而不是删除图像的一部分。

我还添加了另一层试图让橡皮擦工作,并将其命名为“EraserImageView”,并将“蒙版”绘制到其中,然后尝试将该蒙版应用于 MainImageView。

这是我的绘图代码,每次调用 touchesMoved 时都会调用:

编辑: 将橡皮擦的代码添加到我的绘图代码中。

 if eraser {
            let linePath = UIBezierPath()

            for (index, point) in line.enumerated() {
                if index == 0 {
                    midPoint = CGPoint(
                        x: (point.x + point.x) / 2,
                        y: (point.y + point.y) / 2
                    )
                    linePath.move(to: midPoint!)
                } else {
                    midPoint = CGPoint(
                        x: (point.x + line[index - 1].x) / 2,
                        y: (point.y + line[index - 1].y) / 2
                    )
                    linePath.addQuadCurve(to: midPoint!, controlPoint: line[index - 1])
                }
            }

            let maskLayer = CAShapeLayer()
            maskLayer.lineWidth = brush
            maskLayer.lineCap = .round
            maskLayer.strokeColor = UIColor.black.cgColor
            maskLayer.fillColor = nil
            maskLayer.frame = backgroundImageView.bounds
            maskLayer.path = linePath.cgPath
            //eraserImageView.layer.addSublayer(backgroundImageView.layer)
            eraserImageView.layer.addSublayer(maskLayer)
            eraserImageView.layer.mask = mainImageView.layer            
        }

上面的代码导致所有用户绘图消失,除了“橡皮擦”触摸的部分。我知道我有问题,或者我没有正确使用面罩。有没有人有办法解决吗?

画了一些线条,看起来很棒......

[我画了一些线条,看起来很棒!

当我尝试橡皮擦时,会发生这种情况......

当我开始擦除时,一切都消失了,除了我用橡皮擦碰到的地方。

正如您在上面看到的,我可以画线,但是一旦我将橡皮擦触摸到页面上,它就会删除除我用橡皮擦触摸的部分之外的所有内容。

有谁知道我哪里出错了??

编辑: 如此接近! 当我移动手指时,我能够让橡皮擦去除部分画线。但它不是使用尺寸进行绘制,而是在制作形状。使用橡皮擦后,只要我触摸绘图表面,它就会替换所有“已擦除”的部分。

这是我的新橡皮擦代码:

if eraser {
            //var rect: CGRect = CGRect()
            let linePath = UIBezierPath(rect: mainImageView.bounds)

            for (index, point) in line.enumerated() {
                if index == 0 {
                    midPoint = CGPoint(
                        x: (point.x + point.x) / 2,
                        y: (point.y + point.y) / 2
                    )
                    //rect = CGRect(x: midPoint!.x, y: midPoint!.y, width: brush, height: brush)
                    linePath.move(to: midPoint!)
                } else {
                    midPoint = CGPoint(
                        x: (point.x + line[index - 1].x) / 2,
                        y: (point.y + line[index - 1].y) / 2
                    )
                    //rect = CGRect(x: midPoint!.x, y: midPoint!.y, width: brush, height: brush)
                    linePath.addQuadCurve(to: midPoint!, controlPoint: line[index - 1])
                }
            }

            let maskLayer = CAShapeLayer()
            maskLayer.lineWidth = brush
            maskLayer.lineCap = .round
            maskLayer.strokeColor = UIColor.clear.cgColor
            maskLayer.fillColor = UIColor.black.cgColor
            maskLayer.opacity = 1.0
            maskLayer.path = linePath.cgPath
            maskLayer.fillRule = .evenOdd
            mainImageView.layer.addSublayer(maskLayer)
            mainImageView.layer.mask = maskLayer

        }

结果如下: 在此处输入图像描述

关于如何让橡皮擦像线条一样绘制的任何想法?

编辑:应@DonMag 的要求添加背景“绘图”的代码

import Foundation
import UIKit

class DrawBulletLayer : UIView {

    private var bullet: CAShapeLayer?

    func drawBullets(coordinates: UIImageView, bulletColor: UIColor) -> CALayer {
        let bullet = self.bullet ?? CAShapeLayer()
        let bulletPath = UIBezierPath()

        bullet.contentsScale = UIScreen.main.scale

        var bullets: [CGPoint] = []
        let width = coordinates.frame.width
        let height = coordinates.frame.height

        let widthBullets = CGFloat(width / 55)
        let heightBullets = CGFloat(height / 39)

        var hb: CGFloat?
        var wb: CGFloat?

        for n in 1...39 {
            hb = heightBullets * CGFloat(n)
            for o in 1...55 {
                wb = widthBullets * CGFloat(o)
                bullets.append(CGPoint(x: wb!, y: hb!))
            }
        }

        UIColor.black.setStroke()

        bullets.forEach { point in
            bulletPath.move(to: point)
            bulletPath.addLine(to: point)
        }

        bullet.path = bulletPath.cgPath
        bullet.opacity = 1.0
        bullet.lineWidth = 2.0
        bullet.lineCap = .round
        bullet.fillColor = UIColor.clear.cgColor
        bullet.strokeColor = bulletColor.cgColor

        if self.bullet == nil {
            self.bullet = bullet
            layer.addSublayer(bullet)
        }

        return layer
    }
}

以下是将其添加到 BackgroundImageView 的方式:

func updateTemplate() {
        let templates = TemplatePickerData()
        var loadLayer = templates.loadTemplateIds()
        if loadLayer.count == 0 {
            _ = templates.loadTemplates()
            loadLayer = templates.loadTemplateIds()
        }
        print("this is the template ID: \(templateId)")
        //let templateId = loadLayer[template].value(forKey: "templateId") as! Int
        if template < 0 {
            template = 0
        }

        switch template {
        case 0:
            //scrollView.image = UIImage(named: "habitTracker0")!
            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawBullets.drawBullets(coordinates: scrollView.backgroundImageView, bulletColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        case 1:
            //scrollView.image = UIImage(named: "monthTemplate0")!
            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawNotes.drawLines(coordinates: scrollView.backgroundImageView, lineColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        case 2:
            //scrollView.image = UIImage(named: "habitTracker0")!
            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawNotes2.drawLines(coordinates: scrollView.backgroundImageView, lineColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        default:
            if loadLayer.count > template {
                template = 0
            }
            print("this layer is named: \(loadLayer[template].value(forKey: "templateName") as! String)")
            let layer = loadLayer[template].value(forKey: "templatePath") as! String
            templateId = loadLayer[template].value(forKey: "templateId") as! Int
            let thisTemplate = templates.loadImage(image: layer)

            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawBullets.drawBullets(coordinates: scrollView.backgroundImageView, bulletColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.backgroundImageView.layer.addSublayer(thisTemplate)
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        }
        scrollView.setNeedsDisplay()

        if optionsMenuView.pageNameTextField.text != "" {
            if isYear {
                page = optionsMenuView.savePage(journalName: journalName, monthName: nil, weekName: nil, yearName: yearName, yearPosition: yearPosition, pageDrawingPath: pageDrawingPath, originalName: originalYearName, brushColor: 1, brushSize: brushSizeMenuView.brushSlider.value, templateId: templateId, pageDrawing: scrollView.mainImageView.layer)
            } else {
                page = optionsMenuView.savePage(journalName: journalName, monthName: monthName, weekName: weekName, yearName: nil, yearPosition: nil, pageDrawingPath: pageDrawingPath, originalName: originalWeekName, brushColor: 1, brushSize: brushSizeMenuView.brushSlider.value, templateId: templateId, pageDrawing: scrollView.mainImageView.layer)
            }
        }
        optionsMenuView.templateId = templateId
    }

希望能帮助更多...

4

1 回答 1

11

擦除部分贝塞尔路径会很棘手......您可能需要计算交叉点(笔划宽度,而不仅仅是路径本身)并将现有线条分成多个段。

这是另一种方法 - 不确定它是否适合您,但可能值得考虑:

在此处输入图像描述

“绘图”层可能是您已经拥有的。“橡皮擦”层将包括背景图像,然后“线”(贝塞尔路径)将用作蒙版,因此它似乎会擦除下面的部分层。

最后一行作为黄色“绘图”层:

在此处输入图像描述

最后一行作为“橡皮擦”层:

在此处输入图像描述

这是我用于此的代码。我认为展示这个想法非常简单。没有实际的“绘图”功能——它只是使用一组硬编码的坐标和属性,就好像它们是通过触摸跟踪生成的一样。

当您运行它时,顶部的按钮将添加红色、绿色和蓝色“线条”,然后将在“黄色线条”和“橡皮擦线条”之间切换最后一组点。

//
//  ViewController.swift
//  VectorDrawTest
//
//  Created by Don Mag on 8/8/19.
//

import UIKit

enum LineType: Int {
    case DRAW
    case ERASE
}

class LineDef: NSObject {
    var lineType: LineType = .DRAW
    var color: UIColor = UIColor.black
    var opacity: Float = 1.0
    var lineWidth: CGFloat = 8.0
    var points: [CGPoint] = [CGPoint]()
}

class DrawingView: UIView {

    // the background image
    var bkgImage: UIImage = UIImage() {
        didSet {
            updateBkgImage()
        }
    }

    func updateBkgImage() -> Void {
        // if no layers have been added yet, add the background image layer
        if layer.sublayers == nil {
            let l = CALayer()
            layer.addSublayer(l)
        }
        guard let layers = layer.sublayers else { return }
        for l in layers {
            if let _ = l as? CAShapeLayer {
                // in case we're changing the backgound image after lines have been drawn
                // ignore shape layers
            } else {
                // this layer is NOT a CAShapeLayer, so it's either the first (background image) layer
                // or it's an eraser layer, so update the contents
                l.contents = bkgImage.cgImage
            }
        }
        setNeedsDisplay()
    }

    func undo() -> Void {
        // only remove a layer if it's not the first (background image) layer
        guard let n = layer.sublayers?.count, n > 1 else { return }
        _ = layer.sublayers?.popLast()
    }

    func addLineDef(_ def: LineDef) -> Void {

        if def.lineType == LineType.DRAW {

            // create new shape layer
            let newLayer = CAShapeLayer()

            // set "draw" properties
            newLayer.lineCap = .round
            newLayer.lineWidth = def.lineWidth
            newLayer.opacity = def.opacity
            newLayer.strokeColor = def.color.cgColor
            newLayer.fillColor = UIColor.clear.cgColor

            // create bezier path from LineDef points
            let drawPts = def.points
            let bez = UIBezierPath()
            for pt in drawPts {
                if pt == drawPts.first {
                    bez.move(to: pt)
                } else {
                    bez.addLine(to: pt)
                }
            }
            // set path
            newLayer.path = bez.cgPath

            // add layer
            layer.addSublayer(newLayer)

        } else {

            // create new layer
            let newLayer = CALayer()
            // set its contents to the background image
            newLayer.contents = bkgImage.cgImage
            newLayer.opacity = def.opacity

            // create a shape layer to use as a mask
            let maskLayer = CAShapeLayer()

            // set "draw" properties
            // strokeColor will always be black, because it just uses alpha for the mask
            maskLayer.lineCap = .round
            maskLayer.lineWidth = def.lineWidth
            maskLayer.strokeColor = UIColor.black.cgColor
            maskLayer.fillColor = UIColor.clear.cgColor

            // add mask
            newLayer.mask = maskLayer

            // create bezier path from LineDef points
            let drawPts = def.points
            let bez = UIBezierPath()
            for pt in drawPts {
                if pt == drawPts.first {
                    bez.move(to: pt)
                } else {
                    bez.addLine(to: pt)
                }
            }
            // set maskLayer's path
            maskLayer.path = bez.cgPath

            // add layer
            layer.addSublayer(newLayer)

        }

        setNeedsDisplay()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // update layer frames
        if let layers = layer.sublayers {
            for l in layers {
                l.frame = bounds
            }
        }
    }

}


class DrawViewController: UIViewController {

    let theDrawingView: DrawingView = {
        let v = DrawingView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    let demoButton: UIButton = {
        let v = UIButton()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        v.setTitleColor(.blue, for: .normal)
        v.setTitleColor(.lightGray, for: .highlighted)
        v.setTitle("Draw Red", for: .normal)
        return v
    }()

    let redLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .red
        d.lineWidth = 8.0
        d.points = [
            CGPoint(x: 20, y: 20),
            CGPoint(x: 40, y: 140),
            CGPoint(x: 280, y: 200),
        ]
        return d
    }()

    let greenLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .green
        d.lineWidth = 16.0
        d.points = [
            CGPoint(x: 20, y: 100),
            CGPoint(x: 80, y: 80),
            CGPoint(x: 240, y: 140),
            CGPoint(x: 100, y: 200),
        ]
        return d
    }()

    let blueLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .blue
        d.opacity = 0.5
        d.lineWidth = 24.0
        d.points = [
            CGPoint(x: 250, y: 20),
            CGPoint(x: 150, y: 240),
            CGPoint(x: 100, y: 60),
        ]
        return d
    }()

    let yellowLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .yellow
        d.lineWidth = 32.0
        d.points = [
            CGPoint(x: 30, y: 200),
            CGPoint(x: 250, y: 80),
            CGPoint(x: 250, y: 180),
        ]
        return d
    }()

    let eraserLine: LineDef = {
        let d = LineDef()
        d.lineType = .ERASE
        d.lineWidth = 32.0
        d.points = [
            CGPoint(x: 30, y: 200),
            CGPoint(x: 250, y: 80),
            CGPoint(x: 250, y: 180),
        ]
        return d
    }()

    var testErase = false

    override func viewDidLoad() {
        super.viewDidLoad()

        // add the drawing view
        view.addSubview(theDrawingView)

        // constrain it 300 x 300 centered X and Y
        NSLayoutConstraint.activate([
            theDrawingView.widthAnchor.constraint(equalToConstant: 300),
            theDrawingView.heightAnchor.constraint(equalToConstant: 300),
            theDrawingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            theDrawingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ])

        let imgName = "TheCat"
        if let img = UIImage(named: imgName) {
            theDrawingView.bkgImage = img
        }

        // add a demo button
        view.addSubview(demoButton)

        // constrain it 20-pts from the top, centered X
        NSLayoutConstraint.activate([
            demoButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
            demoButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
            demoButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            ])

        // add the touchUpInside target
        demoButton.addTarget(self, action: #selector(doTest), for: .touchUpInside)
    }

    @objc func doTest(_ sender: Any?) -> Void {

        if let b = sender as? UIButton {

            let t = b.currentTitle

            switch t {
            case "Draw Red":
                theDrawingView.addLineDef(redLine)
                b.setTitle("Draw Green", for: .normal)
            case "Draw Green":
                theDrawingView.addLineDef(greenLine)
                b.setTitle("Draw Blue", for: .normal)
            case "Draw Blue":
                theDrawingView.addLineDef(blueLine)
                b.setTitle("Draw Yellow", for: .normal)
            case "Draw Yellow":
                theDrawingView.addLineDef(yellowLine)
                b.setTitle("Toggle Yellow / Erase", for: .normal)
            default:
                toggle()
            }

        }
    }

    func toggle() -> Void {

        // undo the last action
        theDrawingView.undo()

        // toggle bool var
        testErase = !testErase

        // add either yellowLine or eraserLine
        theDrawingView.addLineDef(testErase ? eraserLine : yellowLine)

    }

}

一切都是通过代码完成的——不@IBOutlets,或者@IBActions——所以只需启动一个新项目并ViewController.swift用上面的代码替换。

于 2019-08-08T14:37:52.830 回答