Code
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathMeasure
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
@Composable
fun RoseCurveSpinner(
modifier: Modifier = Modifier,
petals: Int = 2,
tailLength: Float = .2f
) {
val infinite = rememberInfiniteTransition()
val progress by infinite.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 4_000,
easing = LinearEasing,
)
)
)
Box(
modifier = modifier
.defaultMinSize(24.dp, 24.dp)
.drawWithCache {
val path = createRoseCurve(
center = size.center,
radius = size.center.x,
petals = petals,
resolution = 500,
)
val measure = PathMeasure()
measure.setPath(path, true)
onDrawBehind {
val startDistance = measure.length * progress
val stopDistance = (measure.length * progress) + (measure.length * tailLength)
val segment = Path()
measure.getSegment(
startDistance = startDistance,
stopDistance = stopDistance,
destination = segment
)
val segment2 = Path()
if (stopDistance > measure.length) {
measure.getSegment(
startDistance = 0f,
stopDistance = stopDistance - measure.length,
destination = segment2
)
}
listOf(segment, segment2).forEach { path ->
drawPath(
path = path,
brush = Brush.verticalGradient(
colors = listOf(
Pink400,
Violet300,
Emerald400,
),
),
style = Stroke(
width = 30f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
)
)
}
}
}
)
}
fun Path.moveTo(offset: Offset) = moveTo(offset.x, offset.y)
fun Path.lineTo(offset: Offset) = lineTo(offset.x, offset.y)
fun Path.polarLineTo(
degrees: Float,
distance: Float,
origin: Offset = Offset.Zero
) {
lineTo(polarToCart(degrees, distance, origin))
}
fun polarToCart(
degrees: Float,
distance: Float,
origin: Offset = Offset.Zero
): Offset = Offset(
x = distance * cos(-degrees * (PI / 180)).toFloat(),
y = distance * sin(-degrees * (PI / 180)).toFloat(),
) + origin
fun createRoseCurve(
center: Offset,
radius: Float,
petals: Int,
resolution: Int = 300
): Path {
return Path().apply {
moveTo(center)
for (i in 0..resolution) {
val degrees = (i / resolution.toFloat()) * 360f
polarLineTo(
degrees = degrees,
distance = (radius * sin((degrees * PI / 180) * petals)).toFloat(),
origin = center
)
}
}
}