EmergeTools logo
🙏 Supported by Emerge Tools
The platform for all your mobile performance needs

Snowfall Modifier

A Modifier that adds the effect of snowflakes falling on the composable that it's applied to.

Snowfall Modifier Demo

internal fun Modifier.snowfall() = composed {
var snowflakesState by remember {
mutableStateOf(SnowflakesState(-1, IntSize(0, 0)))
}
LaunchedEffect(Unit) {
while (isActive) {
withFrameNanos { newTick ->
val elapsedMillis =
(newTick - snowflakesState.tickNanos).nanoseconds.inWholeMilliseconds
val wasFirstRun = snowflakesState.tickNanos < 0
snowflakesState.tickNanos = newTick
if (wasFirstRun) return@withFrameNanos
for (snowflake in snowflakesState.snowflakes) {
snowflake.update(elapsedMillis)
}
}
}
}
onSizeChanged { newSize -> snowflakesState = snowflakesState.resize(newSize) }
.clipToBounds()
.drawWithContent {
drawContent()
snowflakesState.draw(drawContext.canvas)
}
}
fun ClosedRange<Float>.random() =
ThreadLocalRandom.current().nextFloat() * (endInclusive - start) + start
fun Float.random() =
ThreadLocalRandom.current().nextFloat() * this
fun Int.random() =
ThreadLocalRandom.current().nextInt(this)
fun IntSize.randomPosition() =
Offset(width.random().toFloat(), height.random().toFloat())
private const val snowflakeDensity = 0.1
private val incrementRange = 0.4f..0.8f
private val sizeRange = 5.0f..12.0f
private const val angleSeed = 25.0f
private val angleSeedRange = -angleSeed..angleSeed
private const val angleRange = 0.1f
private const val angleDivisor = 10000.0f
internal data class SnowflakesState(
var tickNanos: Long,
val snowflakes: List<Snowflake>,
) {
constructor(tick: Long, canvasSize: IntSize) : this(tick, createSnowflakes(canvasSize))
fun draw(canvas: Canvas) {
snowflakes.forEach { it.draw(canvas) }
}
fun resize(newSize: IntSize) = copy(snowflakes = createSnowflakes(newSize))
companion object {
private fun createSnowflakes(canvasSize: IntSize): List<Snowflake> {
val canvasArea = canvasSize.width * canvasSize.height
val normalizedDensity = snowflakeDensity.coerceIn(0.0..1.0) / 500.0
val snowflakesCount = (canvasArea * normalizedDensity).roundToInt()
return List(snowflakesCount) {
Snowflake(
incrementFactor = incrementRange.random(),
size = sizeRange.random(),
canvasSize = canvasSize,
position = canvasSize.randomPosition(),
angle = angleSeed.random() / angleSeed * angleRange + (PI / 2.0) - (angleRange / 2.0)
)
}
}
}
}
private val snowflakePaint = Paint().apply {
isAntiAlias = true
color = Color.White
style = PaintingStyle.Fill
}
internal class Snowflake(
private val incrementFactor: Float,
private val size: Float,
private val canvasSize: IntSize,
position: Offset,
angle: Double
) {
private var position by mutableStateOf(position)
private var angle by mutableStateOf(angle)
fun update(elapsedMillis: Long) {
val increment = incrementFactor * (elapsedMillis / baseFrameDurationMillis) * baseSpeedPxAt60Fps
val xDelta = (increment * cos(angle)).toFloat()
val yDelta = (increment * sin(angle)).toFloat()
position = Offset(position.x + xDelta, position.y + yDelta)
angle += angleSeedRange.random() / angleDivisor
if (position.y > canvasSize.height + size) {
position = Offset(position.x, -size)
}
}
fun draw(canvas: Canvas) {
canvas.drawCircle(position, size, snowflakePaint)
}
}

Have a project you'd like to submit? Fill this form, will ya!

If you like this snippet, you might also like:

Maker OS is an all-in-one productivity system for developers

I built Maker OS to track, manage & organize my life. Now you can do it too!