[Swift] MacOS 그림판 앱 만들기 - 성능 개선

빛트·2022년 6월 13일
2

MACOS-SWIFT-CANVAS

목록 보기
2/3
post-thumbnail

선을 다른 곳에서 그려보자

이제 마우스드래그 이벤트는 마우스의 포인트를 Array에 업데이트만 하고,

다시 그릴 화면이 있다고 알려주기만 합니다

라인을 그리는 메서드는 NSView의 draw(_ dirtyRect: ) 메서드 안에서 호출되고,

Array에 의해 선을 그리게 될 겁니다

class Canvas: NSView {
    var lineWidth = 5
    var lines = [[CGPoint]]()

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        self.drawLines()
    }
    
    override func mouseDown(with event: NSEvent) {
        let currentPoint = CGPoint(x: event.locationInWindow.x, y: event.locationInWindow.y)
        lines.append([currentPoint])
    }
    
    override func mouseDragged(with event: NSEvent) {
        guard var lastLine = lines.popLast() else { return }
        let currentPoint = CGPoint(x: event.locationInWindow.x, y: event.locationInWindow.y)
        
        lastLine.append(currentPoint)
        lines.append(lastLine)
        
        self.needsDisplay = true
    }
}

선을 조금 더 부드럽게 그리기 위해서 addLine() 대신 addCurve()를 사용합니다

extension Canvas {
    func drawLines(){
        NSGraphicsContext.saveGraphicsState()

        guard let context = NSGraphicsContext.current?.cgContext else { return }
        
        context.setLineCap(.round)
        context.setLineWidth(lineWidth)
        context.setStrokeColor(NSColor.red.cgColor)
        context.setBlendMode(.normal)
        context.strokePath()
        context.setAlpha(0.3)
        context.setShouldAntialias(true)

        lines.forEach { line in
            for i in stride(from: 0, to: line.count, by: 3) {
                if i == 0 {
                    context.move(to: line[i])
                }else {
                    context.addCurve(to: line[i], control1: line[i-2], control2: line[i-1])
                }
            }
            context.strokePath()
        }
        
        NSGraphicsContext.restoreGraphicsState()
    }
}

조금은 개선된 것처럼 보입니다

하지만 아직도 그려야 할 point가 늘어날수록 CPU 사용량이 빠르게 올라갑니다

선을 다른 방식으로 그려보자

기존에는 모든 점을 이어서 하나의 경로로 연결했습니다

연결해야하는 점들이 많아질수록 path를 그리기 위한 계산 비용이 증가하기 때문에

이런 방식은 좋지 않습니다

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

이제는 3개의 점이 추가될 때마다 새로운 경로를 그립니다

예를 들어, 7개의 점 ( a, b, c, d, e, f, g ) 가 있을 때

기존에는 이를 모두 이어서 하나의 선을 그렸다면

새로운 방식에서는 a, b, c, d 를 잇는 선 하나와 d, e, f, g 를 잇는 선 하나를 그립니다

점 갯수가 800개를 돌파했을 때의 CPU 사용량이 7x%에서 4x%로 내려갔네요

필요한 영역만 다시 그리자

선이 추가될 때 화면 전체를 다시 그리지 않고 변경된 영역만 그립니다

override func mouseDragged(with event: NSEvent) {
    guard var lastLine = lines.popLast() else { return }
    let currentPoint = CGPoint(x: event.locationInWindow.x, y: event.locationInWindow.y)
    let count = lastLine.count
    
    lastLine.append(currentPoint)
    lines.append(lastLine)
    
    if count > 0 && count % 3 == 0 {
        let rect = getRect(currentPoint, lastLine[count-3], lastLine[count-2], lastLine[count-1])
        self.setNeedsDisplay(rect)
    }
}

// 네개의 점이 속하는 영역을 돌려준다.
func getRect(_ point1: CGPoint, _ point2: CGPoint, _ point3: CGPoint, _ point4: CGPoint) -> CGRect {
    let minX = min(point1.x, point2.x, point3.x, point4.x) - CGFloat(lineWidth / 2) // 선의 굵기만큼 영역을 더한다.
    let minY = min(point1.y, point2.y, point3.y, point4.y) - CGFloat(lineWidth / 2) // 선의 굵기만큼 영역을 더한다.
    
    let maxX = max(point1.x, point2.x, point3.x, point4.x) + CGFloat(lineWidth / 2) // 선의 굵기만큼 영역을 더한다.
    let maxY = max(point1.y, point2.y, point3.y, point4.y) + CGFloat(lineWidth / 2) // 선의 굵기만큼 영역을 더한다.
    
    let width = maxX - minX
    let height = maxY - minY
    
    return CGRect(x: minX, y: minY, width: width, height: height)
}

점 갯수가 800개를 돌파했을 때의 CPU 사용량이 4x%에서 1x%로 내려갔네요

0개의 댓글