Custom Widgets & Custom Painting

Building Paths & Bezier Curves

16 min Lesson 5 of 12

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.
Note: The path is stateful — each command continues from where the previous one ended. Always call 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.

Tip: Picture the control point as a magnet: the curve bends towards it. Placing the control point far from the baseline creates a sharp arc; placing it close gives a gentle bulge.

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.

Warning: Calling 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.