메시 그라데이션

메쉬 그라데이션은 패치의 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. 애니메이션을 보여주는 포인트가 있는 애니메이션 메쉬 그라데이션