[Canvas] 베지어 좌표 Compose로 그려보기

2

가끔 화면을 그리다보면, 아래와 같이 물결무늬로 그려진 레이아웃을 표현해야 할때가 있습니다.
혹은 앱에서 그래프를 표현해야 할때도 있죠.

위와같은 그래프는 베지어가 필요없이 Path()만으로 그릴수 있지만, 아래와 같이 구불구불한 그래프가 필요하다면 베지어를 이용하지 않고 그리는것은 아마 매우 힘들것 같습니다.

그렇다면 베지어 좌표가 무엇인지 한번 알아보는 시간을 가져볼까요?

베지어 좌표란?

베지어 곡선은 부드러운 곡선을 모델링하기 위해 컴퓨터 그래픽에서 널리 사용된다. 커브가 컨트롤 포인트의 볼록한 선체에 완전히 포함되어 있기 때문에 점을 그래픽으로 표시하고 직관적으로 커브를 조작하는 데 사용할 수 있다. 변환 및 회전과 같은 어피니션 변환은 곡선의 제어점에 각각의 변환을 적용하여 곡선에 적용 할 수 있다. (출처 : wikipedia)

베지어 곡선은 컴퓨터그래픽에서 곡선을 모델링하기위해 자주 사용되는 곡선 모델입니다.

베지어곡선은 조절점(Control point)을 사용해 직선을 기울여 곡선으로 만들 수 있습니다.
우리는 이 조절점이 n개일때, n-1차 베지어 곡선이라고 부릅니다.

이해가 안간다면, 아래의 사진을 보실까요?

조절점이 두개인 1차 베지어 곡선은 단순한 직선을 그립니다.

T는 선분(직선 위에서 그 위의 두 점 사이에 한정된 부분)을 얼마나 나아갔는지를 의미하죠.

그렇다면 조절점이 세개인 2차 베지어 곡선은 과연 어떻게 그려질까요?

1차 베지어 곡선처럼, 점은 다음 조절점을 통해 움직입니다.

자, 여기에서 사진속에 보이는 M0과 M1을 잇는선을 하나 그을수 있고, M0과 M1이 이동하면, 같이 이동하게 됩니다.

M0과 M1을 잇는 선에 움직이는 점을 B라고 가정했을때 B는 M0과 M1이 움직이는 속도로 이동하게 됩니다.

이것을 2차 베지에 곡선(Quadratic Bezier Curve) 이라고 합니다.

그렇다면 3차 베지에 곡선은 어떻게 움직일지 예상이 가시나요?

3차 베지에 곡선에서 P3라는 조절점이 추가되었습니다.

또한 (M0 ,M1)만 있었던 2차 베지에 곡선에서 M2라는 움직이는 점이 추가되었고,

B가 움직였던 궤적이 2차 베지에 곡선이였던 반면에 3차 베지에 곡선은 B0와 B1을 잇는 점의 궤적이 3차 베지에 곡선이 되었습니다.

이처럼 조절점이 추가되면 추가될수록, 계속해서 잇는선을 그리게 되고,
마지막 남은 선의 움직이는 궤적이 베지에 곡선인 셈입니다.

그렇다면 4차 베지에 곡선은 아래와 같이 되겠죠?

조절점이 많아지면 많아질수록, 곡선이 좀더 동적이게 변하지만, 실용적인 측면이 없어서
일반적으로 3차 베지에 곡선까지 이용되고, Compose에서도 3차까지만 지원이 된다고 합니다!

Compose 코드상으로 구현해보자!

예제로 위와 같은 View를 이미지없이 그려보도록 하겠습니다.

베지에 곡선을 그리려면 Canvas Scope내에서 호출해야 합니다.

Canvas는 quadraticBezierTo()(2차 베지에) , cubicTo()(3차 베지에)를 지원합니다.

저희는 2차 베지에 곡선을 사용할 것입니다.
어떻게 그려야할지 감이 안잡히신다면 아래 그림을 확인해주세요!

위의그림을 보면, 물결무늬의 크기가 같은것을 알수있습니다.

2차 베지에 곡선으로 그린다면, 첫번째 조절점이 위에서 상하로 대칭된것을 알수 있습니다.

만약 곡선의 반복주기가 100px간격이라고 생각해봅시다!(Canvas이기에 px단위!)

곡선은 양쪽으로 고른 곡선이기 때문에, 두번째 조절점은 x좌표는 50입니다.

세번째 조절점은 x좌표가 100이고, y좌표는 첫번째 조절점과 일치해야 합니다.

이걸 한번 코드로 옮겨 봅시다.

// 일단 첫번째로 할일은 Screen의 Width를 구하는것입니다.

val configuration = LocalConfiguration.current
val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() }


// 물결무늬 그래프가  화면끝까지 보이려면 몇번 보여야 하는지 계산합니다.
val idx = (screenWidthPx / 100).toInt()

val bezierGraphPath = Path()

Canvas(){
//y축으로 이동합니다.
  	bezierGraphPath.moveTo(0f, yAxis)
    
	for (it: Int in 0..idx) {
    	
       bezierGraphPath.apply{
     	(xAxis) + 50f,
        // 두번째 조절점의 y는 상하로 반복됩니다. 
		if (it % 2 == 1) 50f else 150f,
        // 세번째 조절점의 x는 도착점이다.
		(xAxis) + 100f,
		yAxis
        }
	}
}

Recomposition이 빈번해서 만들어진 문제


Compose는 빈번하게 Recomposition이 일어나고 Canvas또한 빈번하게 호출된다.
만약 최초 Composition 에서 그려진 Canvas와 Recomposition때 그려진 Canvas가 불일치한다면 위와같은 현상이 발생한다.

주로 Path에서 색깔을 칠할때 이런문제가 발생하는데, 이럴경우 path1.reset()을 통해 한번 초기화를 시켜주는것이 좋다.

@Composable
fun CanVasCottonView() {
    val density = LocalDensity.current
    val configuration = LocalConfiguration.current
    val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() }
    val idx = (screenWidthPx / 100).toInt()
    val path1 = Path()
    val yAxis = 100f
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .height(150.dp),
    ) {
        path1.reset()
        path1.moveTo(0f, yAxis)
        for (it: Int in 0..idx) {
            val xAxis = it * 100f

            path1.apply {
                quadraticBezierTo(
                    (xAxis) + 50f,
                    if (it % 2 == 1) 50f else 150f,
                    (xAxis) + 100f,
                    yAxis,
                )
            }
        }

        val fillPath = android.graphics.Path(path1.asAndroidPath())
            .asComposePath()
            .apply {
                lineTo(size.width, 0f)
                lineTo(0f, 0f)
                lineTo(0f, yAxis)
                close()
            }
        drawPath(
            path = path1,
            color = Main,
            style = Stroke(
                width = 5f,
                cap = StrokeCap.Round,
            ),
        )

        drawPath(
            fillPath,
            color = Main,
        )
    }
}

베지어 좌표가 애니메이션에 적용이 된다고??

베지어 좌표는 그래프뿐만 아니라, 애니메이션의 움직임에도 적용이 가능합니다.


이징 애니메이션 사이트
위와 같은 사진을 본적 있으신가요?

전부 베지어를 통해 만든 trasition 함수입니다.
바로 애니메이션에 보간을 줄때 사용하곤 하는데요.

위와 같은 함수들을 애니메이션에 적용하면, 움직임을 좀 더 동적으로 보여줄 수 있습니다.

이것이 과연 어떻게 가능할까요?

규칙적인 애니메이션은 사선을 그리면서 애니메이션을 진행합니다.

하지만, 갑자기 속도가 빨라지거나 느려진다면 사선이였던 그래프는 곡선이 될수밖에 없습니다.

Compose의 애니메이션에서는 CubicBezierEasing을 통해 제공하고 있습니다.

읽어 주셔서 감사합니다! <3


참고

Creating a graph in Jetpack Compose

First step in Canvas – (2) Path Basics

cubic-bezier 에 대해서 이보다 잘 정리할 수 없다

css로 애니메이션 베지에 곡선을?

easing.net

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글