diff --git a/README.md b/README.md index 190276c..80761a9 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ Box( .snowfall( color = Color.White, alpha = 0.3f, + fadeThreshold = 0.6f, + fadeThresholdSpread = 0.3f, strokeWidth = 1f, drawPosition = SnowfallDrawPosition.Ahead, snowflakeMinSize = 10.dp, diff --git a/sample/src/commonMain/kotlin/io/github/skeptick/snowfall/sample/RootView.kt b/sample/src/commonMain/kotlin/io/github/skeptick/snowfall/sample/RootView.kt index 7a4a135..2f84d3b 100644 --- a/sample/src/commonMain/kotlin/io/github/skeptick/snowfall/sample/RootView.kt +++ b/sample/src/commonMain/kotlin/io/github/skeptick/snowfall/sample/RootView.kt @@ -49,6 +49,8 @@ import io.github.skeptick.snowfall.compose.snowfall fun RootView() { var color by remember { mutableStateOf(Color.White) } var alpha by remember { mutableFloatStateOf(0.5f) } + var fadeThreshold by remember { mutableFloatStateOf(0.6f) } + var fadeThresholdSpread by remember { mutableFloatStateOf(0.3f) } var strokeWidth by remember { mutableFloatStateOf(1f) } var drawPosition by remember { mutableStateOf(SnowfallDrawPosition.Ahead) } var snowflakeMinSize by remember { mutableStateOf(10.dp) } @@ -63,6 +65,8 @@ fun RootView() { modifier = modifier, color = color, alpha = alpha, + fadeThreshold = fadeThreshold, + fadeThresholdSpread = fadeThresholdSpread, strokeWidth = strokeWidth, drawPosition = drawPosition, snowflakeMinSize = snowflakeMinSize, @@ -80,6 +84,8 @@ fun RootView() { modifier = modifier, color = color, alpha = alpha, + fadeThreshold = fadeThreshold, + fadeThresholdSpread = fadeThresholdSpread, strokeWidth = strokeWidth, drawPosition = drawPosition, snowflakeMinSize = snowflakeMinSize, @@ -89,6 +95,8 @@ fun RootView() { snowflakeDensity = snowflakeDensity, onColorChange = { color = it }, onAlphaChange = { alpha = it }, + onFadeThresholdChange = { fadeThreshold = it }, + onFadeThresholdSpreadChange = { fadeThresholdSpread = it }, onStrokeChange = { strokeWidth = it }, onDrawPositionChange = { drawPosition = it }, onSnowflakeSizeChange = { min, max -> @@ -133,6 +141,8 @@ fun RootView() { private fun Preview( color: Color, alpha: Float, + fadeThreshold: Float, + fadeThresholdSpread: Float, strokeWidth: Float, drawPosition: SnowfallDrawPosition, snowflakeMinSize: Dp, @@ -148,6 +158,8 @@ private fun Preview( .snowfall( color = color, alpha = alpha, + fadeThreshold = fadeThreshold, + fadeThresholdSpread = fadeThresholdSpread, strokeWidth = strokeWidth, drawPosition = drawPosition, snowflakeMinSize = snowflakeMinSize, @@ -172,6 +184,8 @@ private fun Preview( private fun Settings( color: Color, alpha: Float, + fadeThreshold: Float, + fadeThresholdSpread: Float, strokeWidth: Float, drawPosition: SnowfallDrawPosition, snowflakeMinSize: Dp, @@ -181,6 +195,8 @@ private fun Settings( snowflakeDensity: Float, onColorChange: (Color) -> Unit, onAlphaChange: (Float) -> Unit, + onFadeThresholdChange: (Float) -> Unit, + onFadeThresholdSpreadChange: (Float) -> Unit, onStrokeChange: (Float) -> Unit, onDrawPositionChange: (SnowfallDrawPosition) -> Unit, onSnowflakeSizeChange: (Dp, Dp) -> Unit, @@ -207,6 +223,20 @@ private fun Settings( onValueChange = onAlphaChange ) + SliderSelector( + title = "Fade threshold", + selectedValue = fadeThreshold, + valueRange = 0f..1f, + onValueChange = onFadeThresholdChange + ) + + SliderSelector( + title = "Fade threshold spread", + selectedValue = fadeThresholdSpread, + valueRange = 0f..1f, + onValueChange = onFadeThresholdSpreadChange + ) + SliderSelector( title = "Stroke Width", selectedValue = strokeWidth, diff --git a/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/Snowfall.kt b/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/Snowfall.kt index b6dab7f..32944fc 100644 --- a/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/Snowfall.kt +++ b/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/Snowfall.kt @@ -47,6 +47,8 @@ public enum class SnowfallDrawPosition { public fun Modifier.snowfall( color: Color = Color.White, alpha: Float = 0.3f, + fadeThreshold: Float = 1f, + fadeThresholdSpread: Float = 0f, strokeWidth: Float = 1f, drawPosition: SnowfallDrawPosition = SnowfallDrawPosition.Ahead, snowflakes: List = DefaultSnowflakes, @@ -59,6 +61,8 @@ public fun Modifier.snowfall( this then SnowfallElement( color = color, alpha = alpha, + fadeThreshold = fadeThreshold, + fadeThresholdSpread = fadeThresholdSpread, strokeWidth = strokeWidth, drawPosition = drawPosition, snowflakes = snowflakes, @@ -77,6 +81,8 @@ public fun Modifier.snowfall( private data class SnowfallElement( val color: Color, val alpha: Float, + val fadeThreshold: Float, + val fadeThresholdSpread: Float, val strokeWidth: Float, val drawPosition: SnowfallDrawPosition, val snowflakes: List, @@ -92,13 +98,16 @@ private data class SnowfallElement( return Snowfall( color = color, alpha = alpha, + fadeThreshold = fadeThreshold, + fadeThresholdSpread = fadeThresholdSpread, stroke = Stroke(strokeWidth), drawPosition = drawPosition, snowflakes = ArrayList(snowflakes), snowflakeSize = snowflakeMinSize..snowflakeMaxSize, snowflakeSpeed = snowflakeMinSpeed..snowflakeMaxSpeed, snowflakeDensity = snowflakeDensity, - snowfallState = snowfallState + snowfallState = snowfallState, + ) } @@ -111,6 +120,8 @@ private data class SnowfallElement( val speedInvalidationRequired = speedInvalidationRequired(node) node.color = color node.alpha = alpha + node.fadeThreshold = fadeThreshold + node.fadeThresholdSpread = fadeThresholdSpread node.stroke = Stroke(strokeWidth) node.drawPosition = drawPosition node.snowflakes = ArrayList(snowflakes) @@ -150,6 +161,8 @@ private data class SnowfallElement( private class Snowfall( var color: Color, var alpha: Float, + var fadeThreshold: Float, + var fadeThresholdSpread: Float, var stroke: Stroke, var drawPosition: SnowfallDrawPosition, var snowflakes: List, @@ -157,7 +170,7 @@ private class Snowfall( var snowflakeSpeed: ClosedRange, var snowflakeDensity: Float, var snowfallState: SnowfallState, - var pathSizes: FloatArray = snowflakes.pathSizes + var pathSizes: FloatArray = snowflakes.pathSizes, ) : DrawModifierNode, LayoutAwareModifierNode, Modifier.Node() { private var canvasSize = Size.Zero @@ -182,8 +195,11 @@ private class Snowfall( override fun ContentDrawScope.draw() { if (drawPosition == SnowfallDrawPosition.Ahead) drawContent() + val canvasHeight = (fadeThreshold + fadeThresholdSpread).coerceIn(0f, 1f) * size.height - clipRect { + clipRect( + bottom = canvasHeight, + ) { snowfallState.snowflakes.fastForEachIndexed { index, flake -> val scale = flake.scale val path = snowflakes[index % snowflakes.size] @@ -196,7 +212,7 @@ private class Snowfall( scale(scale, offset) translate(offset.x, offset.y) }) { - drawPath(path, color, alpha, stroke) + drawPath(path, color, (alpha * flake.alpha), stroke) } } } @@ -245,6 +261,14 @@ private class Snowfall( x = (x + speed * cos(angle)).coerceIn(-flakeSize, canvasSize.width + flakeSize) y = (y + speed * sin(angle)).coerceIn(-canvasSize.height, canvasSize.height + flakeSize) angle += Random.nextFloat() * AddableAngleRange + + val yPos = (y / canvasSize.height) + + alpha = when { + yPos < fadeThreshold -> 1f + yPos > (fadeThreshold + fadeThresholdSpread) -> 0f + else -> (1 - (yPos - fadeThreshold) / (fadeThresholdSpread * alphaOffset)).coerceIn(0f, 1f) + } if (y == canvasSize.height + flakeSize) recycle(index) } @@ -253,6 +277,7 @@ private class Snowfall( scaleRatio = Random.nextFloat() speedRatio = Random.nextFloat() angle = Random.nextFloat() * SourceAngleRange + alpha = 1f scale = scaleRatio * snowflakeSize / pathSize speed = speedRatio * snowflakeSpeed x = Random.nextFloat() * canvasSize.width diff --git a/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/Extensions.kt b/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/Extensions.kt index 5ba7475..8942041 100644 --- a/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/Extensions.kt +++ b/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/Extensions.kt @@ -3,6 +3,7 @@ package io.github.skeptick.snowfall.compose.internal import androidx.compose.ui.graphics.Path internal val SourceAngleRange = 1.45f..1.55f +internal val AlphaOffsetRange = 0.5f..1f internal val AddableAngleRange = -0.0025f..0.0025f internal operator fun Float.times(range: ClosedRange) = diff --git a/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/SnowfallState.kt b/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/SnowfallState.kt index c47613f..159c0c9 100644 --- a/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/SnowfallState.kt +++ b/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/SnowfallState.kt @@ -11,10 +11,11 @@ internal class SnowfallState( ) { companion object StateSaver : Saver by listSaver( save = { state -> - buildList(state.snowflakes.size * 7) { + buildList(state.snowflakes.size * 8) { state.snowflakes.fastForEach { snowflake -> add(snowflake.x) add(snowflake.y) + add(snowflake.alphaOffset) add(snowflake.angle) add(snowflake.scale) add(snowflake.speed) @@ -25,15 +26,16 @@ internal class SnowfallState( }, restore = { list: List -> SnowfallState( - snowflakes = List((list.size) / 7) { index -> + snowflakes = List((list.size) / 8) { index -> SnowflakeState( x = list[index * 7 + 0], y = list[index * 7 + 1], - angle = list[index * 7 + 2], - scale = list[index * 7 + 3], - speed = list[index * 7 + 4], - scaleRatio = list[index * 7 + 5], - speedRatio = list[index * 7 + 6], + alphaOffset = list[index * 7 + 2], + angle = list[index * 7 + 3], + scale = list[index * 7 + 4], + speed = list[index * 7 + 5], + scaleRatio = list[index * 7 + 6], + speedRatio = list[index * 7 + 7], ) } ) diff --git a/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/SnowflakeState.kt b/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/SnowflakeState.kt index 0773915..efaa6a0 100644 --- a/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/SnowflakeState.kt +++ b/snowfall-compose/src/commonMain/kotlin/io/github/skeptick/snowfall/compose/internal/SnowflakeState.kt @@ -11,6 +11,7 @@ import kotlin.random.Random internal class SnowflakeState( x: Float, y: Float, + alphaOffset: Float, angle: Float, scale: Float, speed: Float, @@ -19,6 +20,8 @@ internal class SnowflakeState( ) { var x by mutableFloatStateOf(x) var y by mutableFloatStateOf(y) + var alpha by mutableFloatStateOf(1f) + var alphaOffset by mutableFloatStateOf(alphaOffset) var angle by mutableFloatStateOf(angle) var scale by mutableFloatStateOf(scale) var speed by mutableFloatStateOf(speed) @@ -28,13 +31,14 @@ internal fun SnowflakeState( canvasSize: Size, pathSize: Float, size: ClosedRange, - speed: ClosedRange + speed: ClosedRange, ): SnowflakeState { val scaleRatio = Random.nextFloat() val speedRatio = Random.nextFloat() return SnowflakeState( x = Random.nextFloat() * canvasSize.width, y = Random.nextFloat() * canvasSize.height * -1f, + alphaOffset = Random.nextFloat() * AlphaOffsetRange, angle = Random.nextFloat() * SourceAngleRange, scale = scaleRatio * size / pathSize, speed = speedRatio * speed,