package com.suno.android.common_ui.components import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.suno.android.common_ui.theme.ButtonPink import com.suno.android.common_ui.theme.ExtendedTheme import com.suno.android.common_ui.theme.Warning import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlin.math.pow import kotlin.math.sqrt import kotlin.random.Random import com.suno.android.common_res.R as CommonResR import com.suno.android.common_ui.R as CommonUIR data class BouncyIsosurfaceMetaball( var x: Float, var y: Float, val radius: Float, val color: Color, var vx: Float, var vy: Float, val animatableX: Animatable, val animatableY: Animatable, ) data class GlitterParticle( var x: Float, var y: Float, val animatableAlpha: Animatable, ) @Composable fun BouncyIsosurfaceMetaballAnimation( modifier: Modifier = Modifier, widthInt: Int = 400, heightInt: Int = 100, stepSize: Int = 10, colors: List = listOf(Warning, ButtonPink), ) { val widthDp = widthInt.dp val heightDp = heightInt.dp val density = LocalDensity.current // inner animation bounds val widthPx = with(density) { widthDp.toPx() } val heightPx = with(density) { heightDp.toPx() } val metaballs = remember { colors.mapIndexed { index, color -> val startingX = widthPx / (index + 1) val startingY = heightPx / (index + 1) BouncyIsosurfaceMetaball( x = startingX, y = startingY, radius = 50f, color = color, vx = 50f, vy = 70f, animatableX = Animatable(index * widthPx / colors.size), animatableY = Animatable(index * heightPx / colors.size), ) } } val glitterParticles = remember { List(200) { GlitterParticle( x = Random.nextFloat() * widthPx, y = Random.nextFloat() * heightPx, animatableAlpha = Animatable(Random.nextFloat()), ) } } val scope = rememberCoroutineScope() LaunchedEffect(Unit) { metaballs.forEach { metaball -> scope.launch { animateBouncyIsosurfaceMetaball(metaball, widthPx, heightPx) } } glitterParticles.forEach { particle -> scope.launch { animateGlitterParticle(particle) } } } Canvas(modifier = Modifier.fillMaxSize()) { // modifier makes no difference it seems drawBouncyIsosurfaceMetaballs(metaballs, widthPx.toInt(), heightPx.toInt(), stepSize) drawGlitterParticles(glitterParticles) } } @Composable fun IsosurfaceButton( modifier: Modifier = Modifier, stepSize: Int = 10, colors: List = listOf(Color.Black, Color.White), isInPlayerMode: Boolean = false, onClick: (() -> Unit)? = null, ) { Button( onClick = { onClick?.invoke() }, modifier = modifier, colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), ) { Box(contentAlignment = Alignment.Center) { BouncyIsosurfaceMetaballAnimation( stepSize = stepSize, colors = colors, ) if (isInPlayerMode) { Icon( imageVector = ImageVector.vectorResource(id = CommonUIR.drawable.create), tint = ExtendedTheme.colors.textPrimary, contentDescription = null, ) } else { Row( modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier.padding(horizontal = 8.dp), imageVector = ImageVector.vectorResource(id = CommonUIR.drawable.create), tint = Color.White, contentDescription = null, ) Text( modifier = Modifier.padding(horizontal = 8.dp), text = stringResource(CommonResR.string.create), color = Color.White, ) } } } } } suspend fun animateBouncyIsosurfaceMetaball( metaball: BouncyIsosurfaceMetaball, width: Float, height: Float, ) { coroutineScope { launch { while (true) { val targetX = if (metaball.vx > 0) width - metaball.radius else metaball.radius val durationX = ((targetX - metaball.animatableX.value) / metaball.vx * 3000).toInt() metaball.animatableX.animateTo( targetValue = targetX, animationSpec = tween(durationMillis = durationX, easing = LinearEasing), ) metaball.vx = -metaball.vx } } launch { while (true) { val targetY = if (metaball.vy > 0) height - metaball.radius else metaball.radius val durationY = ((targetY - metaball.animatableY.value) / metaball.vy * 3000).toInt() metaball.animatableY.animateTo( targetValue = targetY, animationSpec = tween(durationMillis = durationY, easing = LinearEasing), ) metaball.vy = -metaball.vy } } } } suspend fun animateGlitterParticle( particle: GlitterParticle, ) { coroutineScope { launch { while (true) { val randomDuration = Random.nextInt(1000, 3000) particle.animatableAlpha.animateTo( targetValue = 0f, animationSpec = tween(durationMillis = randomDuration, easing = LinearEasing), ) particle.animatableAlpha.animateTo( targetValue = 1f, animationSpec = tween(durationMillis = randomDuration, easing = LinearEasing), ) } } } } fun DrawScope.drawBouncyIsosurfaceMetaballs( metaballs: List, width: Int, height: Int, stepSize: Int, ) { for (x in 0 until width step stepSize) { for (y in 0 until height step stepSize) { val color = calculateIsosurfaceColor(x, y, width, height, metaballs) drawRect( color = color, topLeft = Offset(x.toFloat() - 100, y.toFloat() - 80), size = Size(stepSize.toFloat(), stepSize.toFloat()), ) } } } fun DrawScope.drawGlitterParticles( particles: List, ) { particles.forEach { particle -> drawCircle( color = Color.White.copy(alpha = particle.animatableAlpha.value), radius = 2f, center = Offset(particle.x, particle.y), ) } } fun calculateIsosurfaceColor( x: Int, y: Int, width: Int, height: Int, metaballs: List, ): Color { var influence = 0f var red = 0f var green = 0f var blue = 0f for (metaball in metaballs) { val distance = calculateBouncyIsosurfaceDistance( x1 = x.toFloat(), y1 = y.toFloat(), x2 = metaball.animatableX.value, y2 = metaball.animatableY.value, width = width, height = height, ) if (distance > 1e-6) { // Use a small epsilon to avoid division by zero val currentInfluence = metaball.radius / (distance.pow(2)) influence += currentInfluence red += metaball.color.red * currentInfluence green += metaball.color.green * currentInfluence blue += metaball.color.blue * currentInfluence } } if (influence > 0) { red /= influence green /= influence blue /= influence } return Color( red = red.coerceIn(0f, 1f), green = green.coerceIn(0f, 1f), blue = blue.coerceIn(0f, 1f), ) } fun calculateBouncyIsosurfaceDistance( x1: Float, y1: Float, x2: Float, y2: Float, width: Int, height: Int, ): Float { val normalizedX1 = x1 / width val normalizedY1 = y1 / height val normalizedX2 = x2 / width val normalizedY2 = y2 / height val dx = normalizedX2 - normalizedX1 val dy = normalizedY2 - normalizedY1 return sqrt(dx * dx + dy * dy) } @Preview(showBackground = true) @Composable private fun BouncyIsosurfaceMetaballAnimation_Preview() { BouncyIsosurfaceMetaballAnimation( widthInt = 500, heightInt = 900, colors = listOf( Warning, ButtonPink, Color.Yellow, Color.Cyan, ), ) } @Preview(showBackground = true) @Composable private fun BouncyIsosurfaceMetaballButton_Preview() { IsosurfaceButton( modifier = Modifier .fillMaxWidth() .height(100.dp) .padding(16.dp), colors = listOf(Color.Black, Color.White, Color.Red), ) } @Preview(showBackground = true) @Composable private fun BouncyIsosurfaceMetaballButton_Preview2() { Box(modifier = Modifier.fillMaxSize()) { IsosurfaceButton( modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() .height(100.dp) .padding(16.dp), ) } } @Preview(showBackground = true) @Composable private fun BouncyIsosurfaceMetaballButton_Preview_Fab() { Box(modifier = Modifier.width(100.dp)) { IsosurfaceButton( modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() .height(100.dp) .width(100.dp) .padding(16.dp), isInPlayerMode = true, ) } }