[Swift] MacOS 그림판 앱 만들기 - 실행취소, 지우기

빛트·2022년 6월 20일
2

MACOS-SWIFT-CANVAS

목록 보기
3/3
post-thumbnail

Undo

마지막에 그려졌던 라인을 배열에서 삭제하고 시스템에 알려주면 끝~!

func undo(){
    if lines.popLast() != nil {
        self.needsDisplay = true;
    }
}

Erase

우선, 펜의 타입을 정의합니다

enum PenType : Int32 {
    case normal = 0
    case highlighter = 1
    case eraser = 2
}

지우개는 Canvas의 background를 캡쳐한 이미지를 펜의 Color로 사용합니다

이를 위해서 canvas의 background color를 set해주고 캡쳐하도록 했습니다

var backgroundImage: NSImage?

override func awakeFromNib() {
    guard let rep = self.bitmapImageRepForCachingDisplay(in: self.bounds) else { return }
    self.cacheDisplay(in: self.bounds, to: rep)
    let image = NSImage(size: self.bounds.size)
    image.addRepresentation(rep)
    
    self.backgroundImage = image
}

override func draw(_ dirtyRect: NSRect) {
    super.draw(dirtyRect);
    
    NSColor.black.setFill()
    bounds.fill()
    
    self.drawLines();
}

이제 이 이미지를 선을 그릴 때 사용하도록 합니다

var penType : PenType = .normal

func drawLines(){
    guard let context = NSGraphicsContext.current?.cgContext else { return }
    
    context.setLineCap(.round);
    context.setLineWidth(5);
    context.setStrokeColor(NSColor.red.cgColor);
    context.setBlendMode(.normal);
    context.strokePath();
    context.setAlpha(1);
    context.setShouldAntialias(true);
    
    if self.penType == .eraser {
        guard let backgroundImage = self.backgroundImage else {
            return
        }
        
        let color = NSColor(patternImage: backgroundImage)
        context.setStrokeColor(color.cgColor)
        context.setFillColor(color.cgColor)
    }
    
    lines.forEach { line in
        for i in stride(from: 0, to: line.count, by: 3) {
            if i != 0 {
                context.move(to: line[i-3]);
                context.addCurve(to: line[i], control1: line[i-2], control2: line[i-1]);
                context.strokePath();
            }
        }
    }
}

지워지긴 하지만 뭔가 이상합니다

펜이 지나간 영역의 다른 line도 eraser type으로 다시 그려지는 현상이 발생합니다

처음엔 각각의 line마다 type을 저장하는 방법을 생각했지만,

line을 그리는 동작이 끝날 때마다 화면을 이미지로 저장하고 이를 활용하기로 했습니다

( 전자칠판을 구현할때는 통신을 위해 line마다 type을 저장하기는 해야할 것 같습니다 )

이렇게 하면 line을 저장하는 배열도 한단계 간단해집니다

// var lines = [[CGPoint]]();
var line = [CGPoint]()
var bitmapImgs = [NSImage]() // bitmap data vs NSImage ???????

override func mouseDown(with event: NSEvent) {
    let currentPoint = CGPoint(x: event.locationInWindow.x, y: event.locationInWindow.y)
    line.append(currentPoint)
}

override func mouseDragged(with event: NSEvent) {
    let newTouchPoint = CGPoint(x: event.locationInWindow.x, y: event.locationInWindow.y)
    let count = line.count
    
    line.append(newTouchPoint)
    
    if count > 0 && count % 3 == 0 {
        let rect = getRect(newTouchPoint, line[count-3], line[count-2], line[count-1])
        self.setNeedsDisplay(rect)
    }
}

override func mouseUp(with event: NSEvent) {
    NSGraphicsContext.saveGraphicsState()
    
    guard let rep = bitmapImageRepForCachingDisplay(in: self.bounds) else { return }
    self.cacheDisplay(in: self.bounds, to: rep)
    
    let image = NSImage(size: self.bounds.size)
    image.addRepresentation(rep)
    
    bitmapImgs.append(image)
    line.removeAll()
    
    NSGraphicsContext.restoreGraphicsState()
}
func drawLines(){
    ...
//    lines.forEach { line in
      for i in stride(from: 0, to: line.count, by: 3) {
          if i != 0 {
              context.move(to: line[i-3]);
              context.addCurve(to: line[i], control1: line[i-2], control2: line[i-1]);
              context.strokePath();
          }
      }
//    }
}

undo도 다음과 같이 변경되어야 합니다

func undo(){
    if bitmapImgs.popLast() != nil {
        self.needsDisplay = true;
    }
}

다른 일정이 있어 이정도 진행하고 마무리했습니다

상당히 재미있게 준비했던 프로젝트라 킥오프를 기다렸으나... :(

해당 프로젝트는 엎어졌지만, 좋은 경험이었다고 생각되어서 공유합니다

profile
https://kangbit.github.io/posts

0개의 댓글