Building Paths & Bezier Curves
Building Paths & Bezier Curves
The Path class is the backbone of complex shape rendering in Flutter's custom painting system. Instead of relying on predefined primitives like rectangles or circles, you construct arbitrary shapes by issuing a sequence of drawing commands — moving a virtual pen, drawing lines, and sweeping curves — then handing the finished path to canvas.drawPath() for a single, efficient render call.
The Path Object and Coordinate System
A Path is a description of one or more contours (closed or open outlines). The canvas coordinate system places the origin (0, 0) at the top-left corner of the painted area, with x increasing to the right and y increasing downward. Every coordinate you pass to Path methods is in logical pixels relative to that origin.
- moveTo(x, y) — Lifts the pen and sets a new starting point without drawing anything. Use this to begin a new contour or to start a disconnected subpath.
- lineTo(x, y) — Draws a straight line from the current point to
(x, y). - quadraticBezierTo(cpX, cpY, x, y) — Draws a quadratic Bézier curve using one control point
(cpX, cpY)and ending at(x, y). - cubicTo(cp1X, cp1Y, cp2X, cp2Y, x, y) — Draws a cubic Bézier curve using two control points and ending at
(x, y). - close() — Connects the current point back to the start of the current contour with a straight line, sealing the shape.
moveTo before the first drawing command, or Flutter will implicitly start at (0, 0).Drawing a Custom Triangle
The simplest closed path you can build is a triangle. Move to the apex, draw two lines to the base corners, then close the path:
Triangle with moveTo, lineTo, and close
class TrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF1976D2)
..style = PaintingStyle.fill;
final path = Path()
..moveTo(size.width / 2, 0) // apex (top-center)
..lineTo(size.width, size.height) // bottom-right
..lineTo(0, size.height) // bottom-left
..close(); // back to apex
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Notice the cascade operator (..) applied to the Path() constructor. Each method on Path returns void, but the cascade keeps your code concise by chaining calls on the same object.
Quadratic Bézier Curves
A quadratic Bézier curve is defined by three points: the implicit starting point (current pen position), one control point, and the end point. The curve is "attracted" toward the control point without passing through it. This makes quadratic curves ideal for smooth arcs, wave shapes, and speech bubbles.
Wave banner using quadraticBezierTo
class WavePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF26A69A)
..style = PaintingStyle.fill;
final path = Path()
..moveTo(0, size.height * 0.6)
// First wave crest
..quadraticBezierTo(
size.width * 0.25, size.height * 0.4, // control point
size.width * 0.5, size.height * 0.6, // end point
)
// Second wave crest
..quadraticBezierTo(
size.width * 0.75, size.height * 0.8, // control point
size.width, size.height * 0.6, // end point
)
..lineTo(size.width, size.height)
..lineTo(0, size.height)
..close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Cubic Bézier Curves
A cubic Bézier curve introduces a second control point, giving you independent control over the curve's entry and exit angles. This extra degree of freedom is what vector design tools like Figma and Illustrator use for smooth compound curves.
- The first control point governs the tangent direction at the start of the curve.
- The second control point governs the tangent direction at the end of the curve.
- Placing both control points on the same side of the path creates an S-curve; placing them symmetrically creates a balanced arc.
Teardrop shape using cubicTo
class TeardropPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFFE53935)
..style = PaintingStyle.fill;
final cx = size.width / 2;
final path = Path()
..moveTo(cx, size.height) // bottom tip
..cubicTo(
0, size.height * 0.7, // cp1: lower-left
0, size.height * 0.1, // cp2: upper-left
cx, 0, // top
)
..cubicTo(
size.width, size.height * 0.1, // cp1: upper-right
size.width, size.height * 0.7, // cp2: lower-right
cx, size.height, // bottom tip again
)
..close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Combining Path Segments
Real-world shapes mix all four commands freely. A single Path can contain multiple moveTo calls to create subpaths (separate disconnected contours). Flutter renders them all in one drawPath call, which is far more efficient than multiple individual draw calls.
close() connects back to the start of the current contour only, not the very first point of the entire path. If you have used moveTo to start a new subpath, close() closes that subpath independently.Summary
The Path API gives you precise, expressive control over vector shapes in Flutter. The key methods are moveTo (position), lineTo (straight segments), quadraticBezierTo (one-control-point curves), and cubicTo (two-control-point curves), finished with close() and rendered via canvas.drawPath(path, paint). Mastering these four primitives lets you build virtually any shape — icons, charts, decorative backgrounds, and custom UI components — without importing a single image asset.