Learn Cubic Bézier curves in Jetpack Compose

Learn Cubic Bézier curves in Jetpack Compose
A Cubic Bézier curve is a type of curve used in graphics and design, defined by four control points. It is one of the most commonly used Bézier curves, especially in vector graphics, animation, and UI design. The curve is calculated using the following formula:

Formula for Cubic Bézier Curve

The cubic Bézier curve is defined by four points:
  1. \(P_0\)  (start point)
  2. \(P_1\) (first control point)
  3. \(P_2\) (second control point)
  4. \(P_3\) (end point)
The cubic Bézier curve B(t) at any value of t (where t ranges from 0 to 1) is calculated using the formula:
\(B(t) = (1-3)^{3}.P_0+3(1-t)^{2}.t.P_1+3(1-t).t^{2}.P_2+t^{3}.P_3\)

Where:

  • \(P_0\) is the starting point.
  • \(P1\) and \(P_2\) are control points, which influence the curve's direction.
  • \(P_3\) is the endpoint.
  • t is a value between 0 and 1 that determines the position on the curve.

How Cubic Bézier curves work:

  • Start Point \(P_0): ​This is where the curve begins.
  • Control Points \(P_1\) and \(P_2): These points "pull" the curve towards them, determining the direction and steepness of the curve.
  • End Point \(P_3\): This is where the curve ends.
  • t: The parameter that moves from 0 (start) to 1 (end), which determines the curve's progression along the line between the start and end points.

How to draw heart shapes using Jetpack Compose?

1. Drawing a Static Cubic Bézier in Canvas

Compose’s Canvas gives you a Path API with a cubicTo(...) call exactly for Béziers. Here’s a simple Composable that draws our example S-curve from 
(0,0)  to (300,0) with control points at (0,150) and (300,150):
package com.cdingbihar.StaticBezierDemo

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp

// The Composable
@Composable
fun StaticBezierDemo() {
Box(Modifier.fillMaxSize()) {
// A fixed-size canvas to draw into
Canvas(modifier = Modifier.size(320.dp)) {
// `size` is now in scope here:
val p0 = Offset(10f, size.height - 10f)
val p1 = Offset(10f, 10f)
val p2 = Offset(size.width - 10f, 10f)
val p3 = Offset(size.width - 10f, size.height - 10f)

// Build the path
val path = Path().apply {
moveTo(p0.x, p0.y)
cubicTo(
p1.x, p1.y, // first control point
p2.x, p2.y, // second control point
p3.x, p3.y // end point
)
}

// Draw the curve
drawPath(
path = path,
color = Color.Blue,
style = Stroke(width = 5f, cap = StrokeCap.Round)
)

// Draw the control points
listOf(p0, p1, p2, p3).forEach { point ->
drawCircle(Color.Red, radius = 8f, center = point)
}
}
}
}
  • moveTo() jumps to the start P0
  • cubicTo(cx1, cy1, cx2, cy2, x3, y3) adds your Bézier segment.
  • Finally we stroke it in blue and mark the four defining points in red so you can see how control handles pull the curve.
Static Bezier Demo

2. Animating the Control Handles

One of the biggest joys of Compose is built-in animation. Let’s tween our control points so you can watch the shape breathe:
package com.cdingbihar.aniimatedbezier

import androidx.compose.animation.core.FastOutSlowInEasing
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.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp

@Composable
fun AnimatedBezier() {
val modifier = Modifier.size(320.dp)
// Animate from “flat” to “arched” control points
val progress = rememberInfiniteTransition()
.animateFloat(
initialValue = 0.1f,
targetValue = 0.9f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
)
).value

Canvas(modifier = modifier) {
val w = size.width
val h = size.height

val p0 = Offset(10f, h - 10f)
// control points slide horizontally as `progress` changes
val p1 = Offset(10f + progress * w * 0.4f, 10f)
val p2 = Offset(w - 10f - progress * w * 0.4f, 10f)
val p3 = Offset(w - 10f, h - 10f)

val path = Path().apply {
moveTo(p0.x, p0.y)
cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
}

drawPath(path, color = Color.Magenta, style = Stroke(6f, cap = StrokeCap.Round))

// draw handles
drawLine(Color.Gray, p0, p1, strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f,10f),0f))
drawLine(Color.Gray, p2, p3, strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f,10f),0f))
listOf(p0, p1, p2, p3).forEach { drawCircle(Color.Green, 6f, it) }
}
}
  • We use rememberInfiniteTransition() + animateFloat(...) to shuttle progress back and forth.
  • p1.x and p2.x shift based on that progress, so the curve’s bend “breathes.”
  • Dashed gray lines connect handle pairs (start→control1, control2→end) just like vector‐editing UIs.
Animated Bezier


3. Why This Matters in Compose

Custom Shapes & Clipping

You can use cubic Béziers to define arbitrary clip paths (clipPath { … }), letting you craft wavy cards, speech bubbles, or any freeform container.

Vector‐Drawable Interop

Compose’s Path is essentially the same model as Android’s VectorDrawable pathData. If you’ve created an SVG with <path d="M … C …" />, you can translate those commands directly into Compose.

Motion & Gestures

You can let users drag control points at runtime—just track touch events, update p1/p2, and recomposite. It’s the basis for interactive curve editors or sketch apps.

4. Putting It All Together

You might combine these pieces into a little UI:
package com.cdingbihar.beziereditor

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt


@Composable
fun BezierEditor() {
var handle1 by remember { mutableStateOf(Offset(100f, 50f)) }
var handle2 by remember { mutableStateOf(Offset(200f, 50f)) }

Box(Modifier.fillMaxSize().systemBarsPadding()) {
Canvas(Modifier.matchParentSize()) {
// draw cubic with dynamic handles
Path().apply {
moveTo(50f, size.height - 50f)
cubicTo(
handle1.x, handle1.y,
handle2.x, handle2.y,
size.width - 50f, size.height - 50f
)
}.also { path ->
drawPath(path, Color.Cyan, style = Stroke(8f))
}
}

// Draggable handle UIs
Handle(handle1) { handle1 = it }
Handle(handle2) { handle2 = it }
}
}

@Composable
fun Handle(pos: Offset, onDrag: (Offset) -> Unit) {
// A tiny, draggable circle
Box(
Modifier
.offset { IntOffset(pos.x.roundToInt() - 20, pos.y.roundToInt() - 20) }
.size(40.dp)
.pointerInput(Unit) {
detectDragGestures { _, dragAmount ->
onDrag(pos + Offset(dragAmount.x, dragAmount.y))
}
}
.background(Color.Yellow, shape = CircleShape)
)
}
Bezier Editor 1

Bezier Editor 2

Bezier Editor 3


This mini-editor lets you grab the two yellow handles and see the Bézier curve update instantly. Under the hood Compose’s recomposition and Canvas are doing exactly the weighted-blend math we walked through earlier—only you never have to write (1 - t)³ by hand!
Previous Post Next Post

Contact Form