[Android] 모서리 둥근 별 다시 그리기

uuranus·2024년 7월 19일
0
post-thumbnail

지난번의 실수

지난번에 둥근 모서리 별을 그리는 포스팅에서 원의 반지름을 통해 구한 좌표값과 실제 계산된 원의 반지름이 차이가 나서 다시 계산을 해줘야 하는 문제가 있었다.

나는 이걸 소수점 계산 사이에서 발생한 오차라고 생각했는데 다시 한 번 살펴보니 내 실수가 있었다.

내각 계산하기

원인은 꼭짓점의 수를 바꿔보다가 알았다.
꼭짓점의 개수가 3개가 되면 원 좌표값이 NaN가 출력되면서 죽었다.

원의 중심좌표를 구할 때 원의 반지름 / sin(내각의 크기/2) 방식을 이용해서 구했는데 내각의 크기가 0이라서 0을 나눈 꼴이 되어서 무한대가 되면서 죽어버렸던 것이다.

실제 n=3인 star polygon

실제로는 이런 모양이 될 텐데 왜 내각이 0이 나왔을까?

내가 내각의 크기를 잘못 계산했다..!

원의 외부 꼭짓점, 내부 꼭짓점 크기 구하기

여기서 정다각형의 내각의 크기와 별 모양의 내부 각도의 크기가 동일하다고 생각했는데 마름모가 아니기 때문에 같은 각도가 될 수 없다.

그래서 내각 크기를 다각형 내각의 합 방식이 아니라 좌표 점을 통해 atan으로 구하기로 하였다.

val (cX, cY) = vertices[0]
val (pX, pY) = vertices[(vertices.size - 1) % vertices.size]
val (nX, nY) = vertices[1]
val (nnX, nnY) = vertices[2]

val anglePrev = PI / 2 - atan((pY - cY) / (cX - pX))
val angleNext = PI / 2 - atan((nY - cY) / (nX - cX))

val outerVertexHalfAngle = (anglePrev + angleNext) / 2
val outerVertexHalfAngleDegree = outerVertexHalfAngle.toDegree()

val anglePrevInner = PI - atan((nY - cY) / (nX - cX))
val angleNextInner = atan((nnY - nY) / (nnX - nnX))

val innerVertexHalfAngle = (anglePrevInner + angleNextInner) / 2
val innerVertexHalfAngleDegree = innerVertexHalfAngle.toDegree()

제일 첫번째 꼭짓점은 항상 위로 뾰족한 모양으로 그려지기 때문에 양수값이 될 수 있도록 조절해서 계산했다.

원의 중심좌표 구하기

이제 제대로 내각을 구했기 때문에 sin이 0이 될 수 없기 때문에
이전과 동일하게 코드를 사용할 수 있다.

val circleSinDist = curCornerRadius / sin(curPointHalfAngleDegree.toRadian())

val circleCenter = when {
    curCornerRadius == 0f -> {
        Point(x, y)
    }
    i % 2 == 0 -> {
        Point(
            centerX + (curRadius - circleSinDist).toFloat() * sin(currentCenterAngle).toFloat(),
            centerY - (curRadius - circleSinDist).toFloat() * cos(currentCenterAngle).toFloat()
        )
    }
    else -> {
        Point(
            centerX + (circleSinDist + curRadius).toFloat() * sin(currentCenterAngle).toFloat(),
            centerY - (circleSinDist + curRadius).toFloat() * cos(currentCenterAngle).toFloat()
        )
    }
}

시작 각도 구하기

시작 각도도 저번처럼 동일하게 원의 중심좌표까지의 각도에서 내각/2 만큼의 각도를 빼고 내각만큼의 각도를 sweep하는 식으로 구현하였다.

var centerCurAngle = atan2(
    (y - circleCenter.y.toDouble()),
    (x - circleCenter.x.toDouble())
).toDegree()

centerCurAngle = if (centerCurAngle < 0f) {
    360f + centerCurAngle
} else {
    centerCurAngle
}

var startAngleDegree = if (i % 2 == 0) {
    centerCurAngle - 90f + curPointHalfAngleDegree
} else {
    centerCurAngle + 90f - curPointHalfAngleDegree
}

if (startAngleDegree < 0f) {
    startAngleDegree += 360f
}

최종 결과물

이제는 따로 원의 반지름을 구하지 않아도 잘 적용된다.

전체 코드

val path = Path()

val centerX = size.width / 2
val centerY = size.height / 2
val outerRadius = min(centerX, centerY)
val innerRadius = outerRadius * innerRadiusRatio

val theta = PI / numOfPoints
var currentCenterAngle = 0.0

val vertices = mutableListOf<Point>()
for (i in 0 until numOfPoints * 2) {
    val r = if (i % 2 == 0) outerRadius else innerRadius
    val x = centerX + (r * sin(currentCenterAngle)).toFloat()
    val y = centerY - (r * cos(currentCenterAngle)).toFloat()
    vertices.add(Point(x, y))
    currentCenterAngle += theta
}

val (cX, cY) = vertices[0]
val (pX, pY) = vertices[(vertices.size - 1) % vertices.size]
val (nX, nY) = vertices[1]
val (nnX, nnY) = vertices[2]

val anglePrev = PI / 2 - atan((pY - cY) / (cX - pX))
val angleNext = PI / 2 - atan((nY - cY) / (nX - cX))
val outerVertexHalfAngle = (anglePrev + angleNext) / 2
val outerVertexHalfAngleDegree = outerVertexHalfAngle.toDegree()

val anglePrevInner = PI - atan((nY - cY) / (nX - cX))
val angleNextInner = atan((nnY - nY) / (nnX - nnX))
val innerVertexHalfAngle = (anglePrevInner + angleNextInner) / 2
val innerVertexHalfAngleDegree = innerVertexHalfAngle.toDegree()

val outerCornerRadius = minOf(outerCornerSize, outerRadius)
val innerCornerRadius = minOf(innerCornerSize, innerRadius)

currentCenterAngle = 0.0

for (i in vertices.indices) {
    val (x, y) = vertices[i]

    val curPointHalfAngleDegree = if (i % 2 == 0) {
        outerVertexHalfAngleDegree
    } else {
        innerVertexHalfAngleDegree
    }

    val curCornerRadius = if (i % 2 == 0) {
        outerCornerRadius
    } else {
        innerCornerRadius
    }

    val curRadius = if (i % 2 == 0) outerRadius else innerRadius
    val circleSinDist = curCornerRadius / sin(curPointHalfAngleDegree.toRadian())

    val circleCenter = if (curCornerRadius == 0f) {
        Point(x, y)
    } else if (i % 2 == 0) {
        Point(
            centerX + (curRadius - circleSinDist).toFloat() * sin(currentCenterAngle).toFloat(),
            centerY - (curRadius - circleSinDist).toFloat() * cos(currentCenterAngle).toFloat()
        )
    } else {
        Point(
            centerX + (circleSinDist + curRadius).toFloat() * sin(currentCenterAngle).toFloat(),
            centerY - (circleSinDist + curRadius).toFloat() * cos(currentCenterAngle).toFloat()
        )
    }

    var centerCurAngle = atan2(
        (y - circleCenter.y.toDouble()),
        (x - circleCenter.x.toDouble())
    ).toDegree()

    centerCurAngle = if (centerCurAngle < 0f) {
        360f + centerCurAngle
    } else {
        centerCurAngle
    }

    var startAngleDegree = if (i % 2 == 0) {
        centerCurAngle - 90f + curPointHalfAngleDegree
    } else {
        centerCurAngle + 90f - curPointHalfAngleDegree
    }

    if (startAngleDegree < 0f) startAngleDegree += 360f

    val sweepAngle = if (i % 2 == 0) {
        180f - curPointHalfAngleDegree * 2
    } else {
        curPointHalfAngleDegree * 2 - 180f
    }

    path.arcTo(
        rect = Rect(
            offset = Offset(
                (circleCenter.x.toDouble() - curCornerRadius).toFloat(),
                (circleCenter.y.toDouble() - curCornerRadius).toFloat()
            ),
            size = Size(curCornerRadius * 2, curCornerRadius * 2)
        ),
        startAngleDegrees = startAngleDegree.toFloat(),
        sweepAngleDegrees = sweepAngle.toFloat(),
        forceMoveTo = false
    )

    currentCenterAngle += theta
}

path.close()

깃헙 링크

https://github.com/uuranus/compose-shapes

profile
Frontend Developer

0개의 댓글