Vinyl Animation

Vinyl Animation

Subscribe for Live Previews, in your browser

$3 / month

Code

import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
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.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.withSaveLayer
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import theme.Colors.Blue50
import theme.Colors.Lime500
import theme.Colors.Lime950
import theme.Colors.Neutral950
import theme.Colors.Slate900


@Composable
fun VinylAnimation() {
    Box(
        Modifier
            .fillMaxSize()
            .background(Blue50),
        contentAlignment = Alignment.Center
    ) {
        val infinite = rememberInfiniteTransition()

        val rotation by infinite.animateFloat(
            initialValue = 0f,
            targetValue = 360f,
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = 8000,
                    easing = LinearEasing
                )
            )
        )

        val wobbleRotation by infinite.animateFloat(
            initialValue = -1f,
            targetValue = 1f,
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = 2000,
                    easing = FastOutSlowInEasing
                ),
                repeatMode = RepeatMode.Reverse,
            )
        )

        val wiggleRotation by infinite.animateFloat(
            initialValue = -5f,
            targetValue = 5f,
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = 4000,
                    easing = FastOutSlowInEasing
                ),
                repeatMode = RepeatMode.Reverse,
            )
        )

        Box(
            Modifier
                .size(300.dp)
                .aspectRatio(1f)
                .rotate(-45f)
                .graphicsLayer {
                    rotationY = wobbleRotation
                    rotationZ = wiggleRotation
                }
                .clip(CircleShape),
            contentAlignment = Alignment.Center,
        ) {
            Box(
                Modifier.fillMaxSize()
                    .drawBehind {
                        drawRect(color = Neutral950)
                        rotate(
                            degrees = 0f,
                            pivot = center
                        ) {
                            val spokeCount = 2
                            layer {
                                drawRect(
                                    brush = Brush.radialGradient(
                                        colors = buildList {
                                            add(Color.White.copy(alpha = .4f))
                                            repeat(20) { add(Color.Transparent) }
                                        },
                                        tileMode = TileMode.Repeated,
                                        radius = size.width * .02f,
                                    ),
                                )
                                drawCircle(
                                    brush = Brush.sweepGradient(
                                        colors = buildList {
                                            val spokeColor = Color.Black
                                            add(spokeColor)
                                            repeat(spokeCount) {
                                                add(spokeColor.copy(alpha = 0f))
                                                add(spokeColor.copy(alpha = 0f))
                                                if (it < spokeCount - 1) add(spokeColor)
                                            }
                                            add(spokeColor)
                                        },
                                    ),
                                    blendMode = BlendMode.DstAtop,
                                    radius = size.width * size.height
                                )
                            }

                            drawCircle(
                                brush = Brush.sweepGradient(
                                    colors = buildList {
                                        val spokeColor = Color.White.copy(alpha = .45f)
                                        add(spokeColor)
                                        repeat(spokeCount) {
                                            repeat(4) {
                                                add(spokeColor.copy(alpha = 0f))
                                            }
                                            if (it < spokeCount - 1) add(spokeColor)
                                        }
                                        add(spokeColor)
                                    },
                                ),
                                radius = size.width * size.height
                            )
                        }
                    }
            )

            Box(
                Modifier
                    .rotate(rotation)
                    .size(90.dp)
                    .background(
                        color = Lime950,
                        shape = CircleShape,
                    )
                    .padding(10.dp)
                    .background(
                        color = Lime500,
                        shape = CircleShape,
                    )
            ) {
                Text(
                    text = "brat",
                    modifier = Modifier
                        .scale(scaleX = 1.1f, scaleY = 2f)
                        .align(Alignment.Center),
                    color = Slate900,
                    fontWeight = FontWeight.SemiBold,
                    fontSize = 12.sp
                )
            }
        }
    }
}

private fun DrawScope.layer(block: DrawScope.() -> Unit) =
    drawIntoCanvas { canvas ->
        canvas.withSaveLayer(
            bounds = size.toRect(),
            paint = Paint(),
        ) { block() }
    }
Mastodon