눈 내리는 효과처럼 하늘에서 뭔가 천천히 떨어지는 효과를 만들고자 하였다.
그 예시로 포켓몬 슬립에서 빛이 떨어지는 애니메이션 효과가 있는데 이를 분석해서 구현해보기로 하였다.

분석결과
1. 빛들의 위치는 랜덤하게 설정
2. 떨어지는 속도는 동일
3. 떨어지는 방향은 각가 다르지만 하나의 빛은 동일한 방향으로 계속 떨어진다.
radial gradient를 구현하고 그 위에 solid 원을 그려서 구현하였다.
Canvas(
modifier = Modifier
.fillMaxSize()
.background(blueBackground)
) {
lights.forEach { light ->
drawCircle(
brush = Brush.radialGradient(
colors = listOf(yellow, Color.Transparent),
center = light.offset,
radius = 8.dp.toPx()
),
center = light.offset,
)
drawCircle(
color = yellow,
center = light.offset,
radius = 5.dp.toPx()
)
}
}
var lights by remember {
mutableStateOf(
List(20) {
val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
val y = Random.nextInt(screenHeightPx.toInt() / 2).toFloat()
Light(
Offset(x, -y),
Random.nextFloat() * (PI / 2).toFloat() + (PI / 4).toFloat()
)
}
)
}
val amplitude = 6f
LaunchedEffect(Unit) {
while (true) {
lights = lights.map { light ->
val angle = light.angle
val horizontalOffset = amplitude * cos(angle)
val verticalOffset = amplitude * sin(angle)
val newX = light.offset.x + horizontalOffset
val newY = light.offset.y + verticalOffset
if (newX <= 0 || newY >= screenHeightPx) {
val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
val y = Random.nextInt(screenHeightPx.toInt() / 2).toFloat()
light.copy(offset = Offset(x, -y))
} else {
light.copy(offset = Offset(newX, newY))
}
}
kotlinx.coroutines.delay(100L)
}
}
설정된 angle을 통해서 cos, sin을 통해서 다음 위치를 설정해준다.
화면 밖으로 나가면 다시 시작점을 설정해준다.
계산속도에 비해 UI를 그리는 속도가 느려서 죽어버리는 걸 방지하기 위해 delay를 주었다.
포슬립 화면은 스크린 샷이다.
움직이는 건 내가 만든 효과이고 움직이지 않는 건 스크린샷이다.
구분이 잘 안 가도록 잘 구현한 것 같다 ^_^
카카오톡 눈 내리기 효과로도 사용할 수 있다.
@Composable
fun WindBlownEffect() {
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val screenWidthPx = with(density) {
configuration.screenWidthDp * this.density
}
val screenHeightPx = with(density) {
configuration.screenHeightDp * this.density
}
var lights by remember {
mutableStateOf(
List(20) {
val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
val y = Random.nextInt(screenHeightPx.toInt() / 2).toFloat()
Light(
Offset(x, -y),
Random.nextFloat() * (PI / 2).toFloat() + (PI / 4).toFloat()
)
}
)
}
val amplitude = 6f
LaunchedEffect(Unit) {
while (true) {
lights = lights.map { light ->
val angle = light.angle
val horizontalOffset = amplitude * cos(angle)
val verticalOffset = amplitude * sin(angle)
val newX = light.offset.x + horizontalOffset
val newY = light.offset.y + verticalOffset
if (newX <= 0 || newY >= screenHeightPx) {
val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
val y = Random.nextInt(screenHeightPx.toInt() / 2).toFloat()
light.copy(offset = Offset(x, -y))
} else {
light.copy(offset = Offset(newX, newY))
}
}
kotlinx.coroutines.delay(100L)
}
}
val yellow = Color(0xFFFdFF99)
Canvas(
modifier = Modifier
.fillMaxSize()
.background(blueBackground)
) {
lights.forEach { light ->
drawCircle(
brush = Brush.radialGradient(
colors = listOf(yellow, Color.Transparent),
center = light.offset,
radius = 8.dp.toPx()
),
center = light.offset,
)
drawCircle(
color = yellow,
center = light.offset,
radius = 5.dp.toPx()
)
}
}
}
data class Light(
val offset: Offset,
val angle: Float,
)