[Core Graphics] CGMutablePath

Emily·2025년 6월 25일
0
post-thumbnail
https://developer.apple.com/documentation/coregraphics/cgmutablepath/

프로젝트 작업 중 UI 구현하기를 제일 재미없어 하는 내가, 코드로 그림을 그리는 걸 공부하게 될 줄이야. 살다보면 이런 피하고 싶은 영역도 맞닥뜨린다. 오늘은 화면에 띄울 도형(shape)에 입힐 경로(path)를 정의하는 방법을 정리하려 한다. CGMutablePath를 이용해 경로를 만들고, SKShapeNode에 입혀 길을 그린다. 노드의 개념과 화면에 띄우는 방법은 SpriteKit을 공부하면 알 수 있다.

CGMutablePath

CGMutablePathCore Graphics 프레임워크에서 제공하는 클래스로, 2D 그래픽 경로(path)를 생성하고 수정할 수 있는 객체다. 벡터 기반의 그래픽 경로를 동적으로 구성할 수 있게 해주며 직선, 곡선, 호(부채꼴) 등 다양한 형태의 경로 요소들을 조합하여 복잡한 도형을 만들 수 있다. 이름에서도 말하듯 CGPath의 가변(mutable) 버전 하위 클래스로, 생성 후에도 경로를 추가하거나 수정할 수 있다.

경로를 생성하기 위해서는 인스턴스를 생성하고 시작점을 설정해준 뒤 경로를 추가하면 된다. 우선 가장 이해하기 쉽게 직선 경로부터 보겠다.

01) 직선 경로

let path = CGMutablePath()					// 인스턴스 생성
path.move(to: CGPoint(x: 100, y: 100))  	// 시작점 설정
path.addLine(to: CGPoint(x: 200, y: 100))  	// 경로 추가 1
path.addLine(to: CGPoint(x: 150, y: 200))  	// 경로 추가 2

CGPoint로 되어있는 경유 지점을 따라 선을 그리며 경로가 만들어지는 것이다. 예시 코드를 SKShapeNode에 입혀 결과를 확인해보겠다.

let shape = SKShapeNode(path: path)
shape.lineWidth = 10		// 선 굵기
shape.strokeColor = .blue	// 선 색상
addChild(shape)

closeSubpath()

경로를 닫아주는 메소드로, 호출할 경우 시작점과 마지막 점이 이어진다.

let path = CGMutablePath()					// 인스턴스 생성
path.move(to: CGPoint(x: 100, y: 100))  	// 시작점 설정
path.addLine(to: CGPoint(x: 200, y: 100))  	// 경로 추가 1
path.addLine(to: CGPoint(x: 150, y: 200))  	// 경로 추가 2
path.closeSubpath()							// 경로 닫기

02) 곡선 경로

곡선 경로에는 두가지가 있는데 아치 모양의 2차 베지어 곡선(Quadratic Curve)와 S자 모양의 3차 베지어 곡선(Cubic Curve)이 있다.

우선 2차 베지어 곡선을 만들기 위해서는 시작점과 끝점, 그리고 그 사이에 제어점이 필요하다.

let path = CGMutablePath()
path.move(to: CGPoint(x: 50, y: 200))	// 시작점
path.addQuadCurve(
	to: CGPoint(x: 250, y: 200),      	// 끝점
	control: CGPoint(x: 150, y: 100)   	// 제어점
)

3차 베지어 곡선은 제어점이 하나 더 필요하다.

let path = CGMutablePath()
path.move(to: CGPoint(x: 50, y: 150))	// 시작점
path.addCurve(
    to: CGPoint(x: 250, y: 150),       	// 끝점
    control1: CGPoint(x: 100, y: 50),   // 제어점 1
    control2: CGPoint(x: 200, y: 250)   // 제어점 2
)

이 외에도 여러가지 도형도 그릴 수 있다. 지금은 필요 없으니 정리는 생략하겠다.

곡선 잇기

  • addPath를 통해 2개의 path를 결합하기

addPath라는 메소드를 사용해 2개의 경로를 결합한 combinedPath를 생성해보았다.

let path1 = CGMutablePath()
path1.move(to: CGPoint(x: 10, y: 150))
path1.addCurve(
	to: CGPoint(x: 160, y: 150),
	control1: CGPoint(x: 40, y: 50),
	control2: CGPoint(x: 110, y: 250)
)
        
let path2 = CGMutablePath()
path2.move(to: CGPoint(x: 160, y: 150))
path2.addCurve(
	to: CGPoint(x: 320, y: 150),
	control1: CGPoint(x: 190, y: 50),
	control2: CGPoint(x: 260, y: 250)
)
        
let combinedPath = CGMutablePath()
combinedPath.addPath(path1)
combinedPath.addPath(path2)

path1이 끝남과 동시에 path2가 새로 생성된 지점 (160, 150)을 보면 부드럽게 이어지지 않고 살짝 떨어진 게 보인다. 이 이유는 move(to: )로 새로운 시작점을 생성하는 것은 새로운 subpath를 시작하는 것으로, 그림을 그릴 때 펜을 뗐다가 대고 그리는 것과 같다고 한다.

따라서, 여러 개의 커브를 가진 선을 그리고 싶다면 경로를 결합하기 보다는 path 자체에 addCurve를 통해 경로를 이어주는 것이 더 좋다.

  • 연달아 addCurve 해주기
let path = CGMutablePath()
path.move(to: CGPoint(x: 10, y: 150))
path.addCurve(
	to: CGPoint(x: 160, y: 150),
	control1: CGPoint(x: 40, y: 50),
	control2: CGPoint(x: 110, y: 250)
)
                
path.addCurve(
	to: CGPoint(x: 320, y: 150),
	control1: CGPoint(x: 190, y: 50),
	control2: CGPoint(x: 260, y: 250)
)

제어점(control point)에 대해

나는 처음에 제어점(control, control2)의 좌표가 화면에서 보이는 가장 볼록한 부분의 좌표와 일치하는 줄 알았다. 저 좌표들로부터 직선을 당겨서 구불거리게 만들어주는 줄 알았다. 하지만 touchesBegan 메소드를 활용하여 시뮬레이터 상에서 좌표를 눌러 디버깅해보니 그렇지 않았다.

위에서부터 아래로 시작점, 끝점, 제어점1, 제어점2 순이다.
let path = CGMutablePath()
path.move(to: CGPoint(x: 50, y: 150))	// 시작점
path.addCurve(
    to: CGPoint(x: 250, y: 150),       	// 끝점
    control1: CGPoint(x: 100, y: 50),   // 제어점 1
    control2: CGPoint(x: 200, y: 250)   // 제어점 2
)

시작점과 끝점은 할당한 CGPoint값과 일치하지만 제어점 2개는 대략적으로도 비슷하지 않은 걸 확인할 수 있었다. 특히 y값이 너무 달라서 당황했다. 혼자서는 이유를 도저히 추론할 수 없어 지피티에게 물어봤다.

우선 시작점/끝점은 절대 위치로 설정된다고 한다. 하지만 제어점은 시작점과 끝점 간의 곡률을 정의하는 역할을 하는데, 실제 경로에 직접 그려지는 게 아니라 베지어 곡선 계산 과정에서 내부적으로 비율 조정 또는 좌표 변환이 일어난다고 한다.

그래서 그 계산을 어떻게 하면 내가 입력한 좌표에서 곡선이 꺾일까? 궁금해서 물어봤는데

이렇다고 한다.

profile
iOS Junior Developer

0개의 댓글