Created
February 20, 2024 17:06
-
-
Save ardakazanci/9e67adc7571b2dc8190a45d093933e7d to your computer and use it in GitHub Desktop.
Jetpack Compose MetaBall Effect
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Composable | |
fun MetaballEffect() { | |
var targetAngle by remember { mutableStateOf(0f) } | |
val animatedAngle by animateFloatAsState( | |
targetValue = targetAngle, | |
animationSpec = tween(durationMillis = 1000) | |
) | |
var toggle by remember { mutableStateOf(false) } | |
val animatedOffsetX by animateFloatAsState( | |
targetValue = if (toggle) 300f else 200f, | |
animationSpec = tween(durationMillis = 1000) | |
) | |
val animatedOffsetY by animateFloatAsState( | |
targetValue = if (toggle) 400f else 300f, | |
animationSpec = tween(durationMillis = 1000) | |
) | |
var center1 by remember { mutableStateOf(Offset(animatedOffsetX, 200f)) } | |
var center2 by remember { mutableStateOf(Offset(400f, animatedOffsetY)) } | |
var center3 by remember { mutableStateOf(Offset(animatedOffsetX, 400f)) } | |
var center4 by remember { mutableStateOf(Offset(400f, animatedOffsetY)) } | |
LaunchedEffect(Unit) { | |
while (true) { | |
toggle = !toggle | |
delay(2000) | |
} | |
} | |
Canvas(modifier = Modifier | |
.fillMaxSize() | |
.clickable { | |
targetAngle += 45f | |
}.pointerInput(Unit) { | |
detectDragGestures { change, dragAmount -> | |
val dragOffset = Offset(dragAmount.x, dragAmount.y) | |
when { | |
center1.distanceTo(change.position) < 50.dp.toPx() -> { | |
center1 = center1 + dragOffset | |
} | |
center2.distanceTo(change.position) < 50.dp.toPx() -> { | |
center2 = center2 + dragOffset | |
} | |
center3.distanceTo(change.position) < 50.dp.toPx() -> { | |
center3 = center3 + dragOffset | |
} | |
center4.distanceTo(change.position) < 50.dp.toPx() -> { | |
center4 = center4 + dragOffset | |
} | |
} | |
} | |
} | |
) { | |
rotate(degrees = animatedAngle, pivot = center) { | |
val radius = 50.dp.toPx() | |
drawMetaball(center1, center2, radius, this) | |
drawMetaball(center1, center3, radius, this) | |
drawMetaball(center1, center4, radius, this) | |
drawMetaball(center2, center3, radius, this) | |
drawMetaball(center2, center4, radius, this) | |
drawMetaball(center3, center4, radius, this) | |
} | |
} | |
} | |
fun drawMetaball(center1: Offset, center2: Offset, radius: Float, drawScope: DrawScope) { | |
val distance = center1.distanceTo(center2) | |
if (distance <= radius * 3 && distance > 0f) { | |
val u = 2.0 + (distance / (radius * 3)) * 2.0 | |
val v = min(1.0, sqrt(radius * 3 / distance) * 0.5) | |
val angleBetweenCenters = atan2((center2.y - center1.y).toDouble(), (center2.x - center1.x).toDouble()) | |
val angleOffset = acos(((radius - radius * v) / distance).coerceIn(-1.0, 1.0)) | |
val angle1 = angleBetweenCenters + angleOffset | |
val angle2 = angleBetweenCenters - angleOffset | |
val angle3 = angleBetweenCenters + PI - angleOffset | |
val angle4 = angleBetweenCenters + PI + angleOffset | |
val p1 = Offset( | |
(center1.x + radius * cos(angle1)).toFloat(), | |
(center1.y + radius * sin(angle1)).toFloat() | |
) | |
val p2 = Offset( | |
(center1.x + radius * cos(angle2)).toFloat(), | |
(center1.y + radius * sin(angle2)).toFloat() | |
) | |
val p3 = Offset( | |
(center2.x + radius * cos(angle3)).toFloat(), | |
(center2.y + radius * sin(angle3)).toFloat() | |
) | |
val p4 = Offset( | |
(center2.x + radius * cos(angle4)).toFloat(), | |
(center2.y + radius * sin(angle4)).toFloat() | |
) | |
val controlPoint = Offset( | |
(0.5 * (center1.x + center2.x)).toFloat(), | |
(0.5 * (center1.y + center2.y)).toFloat() | |
) | |
val path = Path().apply { | |
moveTo(p1.x, p1.y) | |
cubicTo( | |
((p1.x + u * controlPoint.x) / (1 + u)).toFloat(), | |
((p1.y + u * controlPoint.y) / (1 + u)).toFloat(), | |
((p3.x + u * controlPoint.x) / (1 + u)).toFloat(), | |
((p3.y + u * controlPoint.y) / (1 + u)).toFloat(), | |
p3.x, p3.y | |
) | |
lineTo(p4.x, p4.y) | |
cubicTo( | |
((p4.x + u * controlPoint.x) / (1 + u)).toFloat(), | |
((p4.y + u * controlPoint.y) / (1 + u)).toFloat(), | |
((p2.x + u * controlPoint.x) / (1 + u)).toFloat(), | |
((p2.y + u * controlPoint.y) / (1 + u)).toFloat(), | |
p2.x, p2.y | |
) | |
close() | |
} | |
drawScope.drawPath(path, color = Color(0XFF353535)) | |
} | |
drawScope.drawCircle(color = Color(0XFF353535), center = center1, radius = radius) | |
drawScope.drawCircle(color = Color(0XFF353535), center = center2, radius = radius) | |
} | |
fun Offset.distanceTo(other: Offset): Float { | |
return sqrt((x - other.x).pow(2) + (y - other.y).pow(2)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment