问题
I have been looking everywhere for the answer to a question that has been asked a ton of times. I have spent hours looking through SO and Google. There has to be an answer that isn't going to take a mountain moving effort.
I am working on a vector drawing app and finally got the drawing and undo-ing functionality working. Now I need an eraser :-o
EDIT: Per the great write up from @DonMag I was able to get pretty close to an eraser, but something still isn't quite right. So I am going to try and explain how my views and layers are in the app and why I have done it this way:
Starting from the bottom view/layer to the top...
BackgroundImageView - I am using this image view to hold the "background" for the drawing surface. It is a layer that can be changed and can have new "templates" saved and recalled into. I keep it separate so that the user can't erase the drawing surface. And the background consists of CAShapeLayers that are drawn to represent different paper types.
MainImageView - I am using this image view to do all the drawing that the user initiates. So I touch and drag my finger, and new CAShapeLayer is added to the image view. This keeps the user's drawing separate from the "drawing surface". This is also the place I want the erasing to happen
PageImagesView - I use this view to hold images that the user can add to the page, and move/resize them. I don't want the eraser to effect the image, but if a line drawn in MainImageView crosses over the image and needs to be erased it should let the image show through, and not remove parts of the image.
I also added another layer trying to get the eraser working, and called it "EraserImageView", and was drawing the "mask" into it, then trying to apply that mask to the MainImageView.
Here is my drawing code, called each time touchesMoved is called:
EDIT: Adding the code for eraser into my Drawing code.
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
}
The code above causes all of the user drawing to disappear except the portion that is touched by the "eraser". I know that I have something out of order, or I'm applying the mask incorrectly. Does anyone have a solution?
Drawing some Lines, and it looks great...
[
When I attempt the eraser this is what happens...
As you can see above I can draw lines, but once I touch the eraser to the page it removes everything except for the part I touch with the eraser.
Does anyone know where I am going wrong??
Edit: SO CLOSE! I was able to get the eraser to remove part of the drawn line when I move my finger. But it isn't drawing using the Sizes and it is making shapes. It is also replacing all the "erased" parts as soon as I touch the drawing surface after using the eraser.
Here is my new eraser code:
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
}
Here is the result:
Any ideas on how to get the eraser to draw just like the lines?
EDIT: Adding the code for the background "drawing" at the request of @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
}
}
Here is how it is added to the 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
}
Hope that helps more...
回答1:
Erasing part of a bezier path would be tricky... you'd probably need to calculate intersections (of the stroke width, not just of the path itself) and break existing lines into multiple segments.
Here is another approach - not sure if it will work for you, but might be worth considering:
The "Drawing" layers are probably what you already have. The "Eraser" layer would include the background image, and then the "line" (the bezier path) would be used as a mask, so it would appear to erase portions of the layers below.
With the final line as a yellow "Drawing" layer:
and with the final line as an "Eraser" layer:
Here is the code I used for this. I think it's pretty straight-forward to demonstrate the idea. No actual "drawing" feature -- it just uses a hard-coded set of coordinates and properties as if they had been generated by touch-tracking.
When you run it, the button at the top will add the Red, Green and Blue "lines," and then will toggle the last set of points between a "Yellow line" and an "Eraser line."
//
// 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)
}
}
Everything is done via code - no @IBOutlets
or @IBActions
- so just start a new project and replace ViewController.swift
with the above code.
来源:https://stackoverflow.com/questions/57381552/how-to-create-an-eraser-for-calayer-sublayer-using-a-cashapelayer-in-swift