What You Will Build

A mathematically-drawn heart shape on Canvas that pulses continuously with an infinite transition and explodes in scale when tapped, using spring physics. This combines parametric math curves, Canvas drawing, infinite animations, and gesture handling in a single composable.

Why This Pattern Matters

Like buttons on Instagram, Twitter, and TikTok all use heart animations. The parametric heart curve teaches you mathematical drawing on Canvas, while the spring-based tap animation is the standard pattern for micro-interactions in social media apps.

Step 1: Set Up the Animations

@Composable
fun HeartAnimationScreen() {
    // Tap-triggered spring animation
    val scale = remember { Animatable(1f) }
    val scope = rememberCoroutineScope()

    // Continuous pulse
    val infiniteTransition = rememberInfiniteTransition(label = "heart")
    val pulse by infiniteTransition.animateFloat(
        1f, 1.15f,
        infiniteRepeatable(tween(600), RepeatMode.Reverse),
        label = "pulse"
    )

Step 2: Draw the Parametric Heart

The heart shape uses the parametric equations: x = 16sin^3(t), y = 13cos(t) - 5cos(2t) - 2cos(3t) - cos(4t):

Canvas(Modifier.size(200.dp)) {
    val cx = size.width / 2
    val cy = size.height / 2
    val heartSize = 80f * pulse * scale.value
    val path = Path()

    for (t in 0..360) {
        val rad = Math.toRadians(t.toDouble())
        val x = 16f * sin(rad).toFloat().pow(3) * heartSize / 16f
        val y = -(13f * cos(rad).toFloat()
                - 5f * cos(2 * rad).toFloat()
                - 2f * cos(3 * rad).toFloat()
                - cos(4 * rad).toFloat()) * heartSize / 16f
        if (t == 0) path.moveTo(cx + x, cy + y)
        else path.lineTo(cx + x, cy + y)
    }
    path.close()
    drawPath(path, Color(0xFFFF1744))
}

Step 3: Add Tap-to-Bounce Gesture

Box(
    Modifier.fillMaxSize()
        .background(Color(0xFF1A1A2E))
        .pointerInput(Unit) {
            detectTapGestures {
                scope.launch {
                    scale.animateTo(1.5f,
                        spring(dampingRatio = 0.3f))
                    scale.animateTo(1f, spring())
                }
            }
        },
    contentAlignment = Alignment.Center
) {
    // Canvas goes here
}

Tips and Pitfalls

  • Animatable vs animateFloatAsState: Animatable supports sequential animations (scale up then down) via coroutines. animateFloatAsState only animates to a single target.
  • Spring dampingRatio = 0.3f creates the bouncy overshoot. Default (1.0) would ease in without bounce.
  • The negative Y in the equation flips the heart right-side up (Canvas Y increases downward).
  • Performance: Drawing 360 line segments per frame is fine for a single shape. For multiple hearts, pre-compute the path and cache it with remember.

Min SDK: 21 | Compose BOM: 2024.01.00+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.