메시 그라데이션

메시 그래디언트는 패치의 2D 그리드를 사용하여 복잡한 다방향 색상 전환을 만듭니다. 선형 또는 방사형 그래디언트와 달리 메시 그래디언트는 그리드 전체에서 색상을 부드럽게 보간합니다. 메시 그래디언트를 사용하여 사용자 인터페이스에서 유동적이고 유기적인 미적 요소를 만듭니다.

현재 메시 그라데이션 포인트가 표시된 메시 그라데이션의 예
그림 1. 현재 메시 그래디언트 포인트가 표시된 메시 그래디언트의 예입니다.

주요 개념

메시 그래디언트를 구성하려면 그리드 크기, 꼭짓점, 포인트 간 색상 전환을 정의합니다.

  • 그리드 크기: 메시는 세로축과 가로축을 따라 패치로 분할됩니다. rowscolumns의 그리드에는 (rows+1)×(columns+1)개의 꼭짓점이 포함됩니다. 예를 들어 1×1 메시는 하나의 패치를 형성하는 4개의 꼭짓점으로 구성됩니다.
  • 정규화된 좌표: 모든 꼭짓점 위치는 정규화된 좌표계를 사용하며, 여기서 (0f, 0f)는 그리기 경계의 왼쪽 상단을 나타내고 (1f, 1f)는 오른쪽 하단을 나타냅니다.
  • 베지어 제어 포인트 (탄젠트): 각 꼭짓점에는 최대 4개의 선택적 베지어 제어 포인트가 포함됩니다. 이러한 탄젠트는 인접한 꼭짓점 간의 가장자리 곡률을 지정합니다. Offset.Unspecified를 사용하는 경우 Compose는 패치 간의 부드러운 전환을 보장하기 위해 탄젠트를 추론합니다. 제어 포인트와 함께 4개의 꼭짓점으로 형성된 각 그리드 셀은 베지어 패치를 생성합니다.
  • 색상 보간: 프레임워크는 기본 꼭짓점 간의 색상을 계산합니다. 더 부드러운 색상 이동을 위한 Catmull-Rom 보간의 경우 hasBicubicColortrue로 설정하고, 쌍선형 보간의 경우 false로 설정합니다.

MeshGradientPainter로 그리기

Jetpack Compose에서 MeshGradientPainter 를 사용하여 메시 그래디언트를 렌더링합니다. MeshGradientPainter는 캔버스에 그립니다.

간단한 메시 그래디언트 만들기

기본 정적 메시 그래디언트를 만들려면 크기를 지정하고 구성 블록 내에서 setVertex 함수를 사용하여 모서리 포인트를 배치하고 색상을 할당하여 MeshGradientPainter를 초기화합니다.

val rows = 1
val columns = 1

val gradientPainter = remember {
    MeshGradientPainter(rows, columns) {
        // Parameters: row, column, position, color
        setVertex(0, 0, Offset(0f, 0f), Color.Red)     // Top-Left
        setVertex(0, 1, Offset(1f, 0f), Color.Blue)    // Top-Right
        setVertex(1, 0, Offset(0f, 1f), Color.Green)   // Bottom-Left
        setVertex(1, 1, Offset(1f, 1f), Color.Yellow)  // Bottom-Right
    }
}

Box(
    modifier = modifier
        .aspectRatio(16/9f)
        .fillMaxWidth()
        .paint(gradientPainter)
)

각 모서리에 4가지 색상이 정의된 기본 메쉬 그라데이션
그림 2. 4가지 색상이 있는 기본 메시 그래디언트이며 각 모서리는 4가지 색상 중 하나로 설정됩니다.

특정 베지어 제어 포인트 사용

기본적으로 메시 생성기는 그리드 전환을 원활하게 유지하기 위해 복잡한 계산을 처리합니다. 하지만 특정 색상 섹션을 선택적으로 밀거나 당기거나 날카롭게 꼬집으려면 단일 꼭짓점에서 탄젠트를 명시적으로 맞춤설정할 수 있습니다.

제어 오프셋은 호스트 꼭짓점의 위치를 기준으로 측정됩니다.

val customTangentPainter = remember {
    MeshGradientPainter(rows = 1, columns = 1) {
        // Tweak the top-left vertex to curve outwards to the right and bottom
        setVertex(
            row = 0,
            column = 0,
            position = Offset(0f, 0f),
            color = Color.Magenta,
            rightControlPoint = Offset(0.4f, 0.1f),
            bottomControlPoint = Offset(0.1f, 0.4f)
        )

        // Other points can remain unspecified to use default inferred fallback tangents
        setVertex(0, 1, Offset(1f, 0f), Color.Cyan)
        setVertex(1, 0, Offset(0f, 1f), Color.Blue)
        setVertex(1, 1, Offset(1f, 1f), Color.Black)
    }
}
Box(
    modifier = modifier
        .aspectRatio(16/9f)
        .fillMaxWidth()
        .paint(customTangentPainter)
)

곡선형 왼쪽 상단 점이 있는 메시 그라데이션
그림 3. 베지어 제어 포인트로 왼쪽 상단 꼭짓점을 곡선으로 만듭니다.

고급 그리드 만들기

이 예에서는 3x3 그리드를 보여줍니다. 즉, 16개의 포인트를 지정해야 하며 중간 포인트는 서로 다른 오프셋으로 설정됩니다.

val points = remember {
    listOf(
        Offset(0.0f, 0.0f), Offset(0.3f, 0.0f), Offset(0.7f, 0.0f), Offset(1.0f, 0.0f),
        Offset(0.0f, 0.3f), Offset(0.2f, 0.4f), Offset(0.7f, 0.2f), Offset(1.0f, 0.3f),
        Offset(0.0f, 0.7f), Offset(0.3f, 0.8f), Offset(0.7f, 0.6f), Offset(1.0f, 0.7f),
        Offset(0.0f, 1.0f), Offset(0.3f, 1.0f), Offset(0.7f, 1.0f), Offset(1.0f, 1.0f)
    )
}

val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
        // Row 0
        setVertex(0, 0, points[0], yellow)
        setVertex(0, 1, points[1], orange)
        setVertex(0, 2, points[2], yellow)
        setVertex(0, 3, points[3], purple)

        // Row 1
        setVertex(1, 0, points[4], pink)
        setVertex(1, 1, points[5], yellow)
        setVertex(1, 2, points[6], pink)
        setVertex(1, 3, points[7], purple)

        // Row 2
        setVertex(2, 0, points[8], indigo)
        setVertex(2, 1, points[9], pink)
        setVertex(2, 2, points[10], purple)
        setVertex(2, 3, points[11], indigo)

        // Row 3
        setVertex(3, 0, points[12], purple)
        setVertex(3, 1, points[13], indigo)
        setVertex(3, 2, points[14], pink)
        setVertex(3, 3, points[15], yellow)
    }
}

Box(
    modifier = modifier.padding(32.dp)
        .aspectRatio(16 / 9f)
        .fillMaxWidth()
        .paint(gradientPainter)
        // ...
)

베지어 제어점과 파도 색상이 있는 메쉬 그라데이션과 그 위에 메쉬 점이 표시되어 있습니다.
그림 4. 베지어 제어 포인트와 파동 색상이 있는 메시 그래디언트이며 메시 포인트는 그 위에 표시됩니다.

메시 그래디언트 애니메이션 처리

MeshGradientPainterblock 람다 매개변수는 DrawScope 내에서 실행되므로 변경 가능한 상태를 읽고 관찰할 수 있습니다. 셰이더 또는 비트맵을 다시 할당하지 않고도 시간이 지남에 따라 위치 또는 색상을 애니메이션 처리할 수 있습니다.

val infiniteTransition = rememberInfiniteTransition(label = "meshMovement")
val animatedOffset by infiniteTransition.animateFloat(
    initialValue = -0.1f,
    targetValue = 0.1f,
    animationSpec = infiniteRepeatable(
        animation = tween(2500, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "offset"
)

val coral = Color(255, 90, 90)
val peach = Color(255, 139, 90)
val amber = Color(255, 169, 90)
val sunshine = Color(255, 212, 90)
val indigo = Color(0xFF5856D6)
val pink = Color(0xFFFF2D55)


val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
        // Row 0
        setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
        setVertex(0, 1, Offset(0.3f, 0.0f), peach)
        setVertex(0, 2, Offset(0.7f, 0.0f), amber)
        setVertex(0, 3, Offset(1.0f, 0.0f), sunshine)
        // Row 1
        setVertex(1, 0, Offset(0.0f, 0.3f), pink)
        setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
        setVertex(1, 2, Offset(0.7f, 0.2f) + Offset(animatedOffset, animatedOffset), peach)
        setVertex(1, 3, Offset(1.0f, 0.3f), indigo)

        // Row 2
        setVertex(2, 0, Offset(0.0f, 0.7f), coral)
        setVertex(2, 1, Offset(0.3f, 0.8f) + Offset(animatedOffset, 0f), pink)
        setVertex(2, 2, Offset(0.7f, 0.6f) + Offset(animatedOffset, 0f), sunshine)
        setVertex(2, 3, Offset(1.0f, 0.7f), amber)

        // Row 3
        setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
        setVertex(3, 1, Offset(0.3f, 1.0f), amber)
        setVertex(3, 2, Offset(0.7f, 1.0f), pink)
        setVertex(3, 3, Offset(1.0f, 1.0f), indigo)
    }
}


Box(
    modifier = modifier.padding(32.dp)
        .safeContentPadding()
        .aspectRatio(16 / 9f)
        .fillMaxWidth()
        .paint(gradientPainter)
)

그림 5. 애니메이션을 표시하는 포인트가 있는 애니메이션 처리된 메시 그래디언트입니다.