Really Rounded Polygons
Nick Butcher recently demonstrated a Path
-and-CornerPathEffect
-based method for drawing regular polygons with rounded corners. My library PolygonDrawingUtil solves the same problem but produces noticeably different results:

This inspired me to dig into how CornerPathEffect
is implemented and compare it to the internals of PolygonDrawingUtil. Read on for commentary, code, and conclusions.
The Base Case
The light gray triangle of radius 500px in the animation above is the shape we will be rounding throughout this post. Here it is drawn a little darker for clarity:

All subsequent discussion applies to any n-sided regular polygon.
PolygonDrawingUtil
PolygonDrawingUtil dynamically generates and draws rounded polygon Path
s based on user-specified attributes:
- polygon side count,
- polygon center coordinates,
- polygon radius (center to corner), and
- desired corner radius.
These Path
s use exactly two components: straight lines for sides, and circular arcs for corners.
Below are some examples created using this method. The full circles used to create the rounded corner arcs are shown in light gray for illustration:

This approach creates corners with exact and uniform radii. If the corner radius becomes too large relative to the polygon radius, the drawn shape gracefully degrades to a circle:

The code that constructs these Path
s is shown below. I’m not going to dissect it line by line, but the essential steps are:
- Calculate the length of arc to use for each corner;
- Calculate where each arc should be centered to ensure a smooth join with the sides;
- Draw each arc in turn, using a neat feature of
Path.arcTo
to automatically insert the sides:If the start of the path is different from the path’s current last point, then an automatic lineTo() is added to connect the current contour to the start of the arc.
private void constructRoundedPolygonPath(
int sides,
float centerX,
float centerY,
float polyRadius,
float cornerRadius) {
float arcSweep = 180.0 / sides;
double arcCenterRadius = polyRadius - cornerRadius / sin(radians(90 - arcSweep));
for (int nCorner = 0; nCorner < sides; nCorner++) {
double cornerAngle = 360.0 * nCorner / sides;
float arcCenterX = (float) (centerX + arcCenterRadius * cos(radians(cornerAngle)));
float arcCenterY = (float) (centerY + arcCenterRadius * sin(radians(cornerAngle)));
arcBounds.set(
arcCenterX - cornerRadius,
arcCenterY - cornerRadius,
arcCenterX + cornerRadius,
arcCenterY + cornerRadius);
backingPath.arcTo(
arcBounds,
(float) (cornerAngle - 0.5 * arcSweep),
arcSweep);
}
backingPath.close();
}
CornerPathEffect
PathEffect
s are used to modify how an existing Path
is drawn. Applied via Paint.setPathEffect
, they grant Paint
s some “artistic license”. For example, CornerPathEffect
allows the Paint
to draw rounded corners in place of any sharp corners. Users can specify a corner “radius” that influences the amount of rounding applied. Note that the Path
itself is never altered by the application of a PathEffect
.
CornerPathEffect
draws rounded polygon Path
s using two components: straight lines for sides, and quadratic Bézier curves for corners.
Below are some examples created using this method:

This approach creates corners with nonuniform radii. Note too that the actual corner radius does not seem to match the specified corner radius if we interpret it as a value in pixels (this discrepancy is also illustrated by the earlier animation which shows that PolygonDrawingUtil corner roundness and CornerPathEffect
corner roundness differ for the same input radius.)
If the corner radius becomes too large relative to the polygon radius, the drawn shape settles in this form:

As far as I can tell, there’s no easy way to predict what this degenerate shape will look like ahead of time.
The relevant native code from SkCornerPathEffect.cpp is below1. For each line in the original path, the essential steps are:
- Calculate the control and end point locations for the corner curve;
- Draw a quadratic Bézier curve using these points;
- If necessary (i.e. the corner radius is small compared to the polygon radius), draw the remainder of the original straight side.
bool SkCornerPathEffect::filterPath(SkPath* dst, const SkPath& src, SkStrokeRec*, const SkRect*) const {
SkPath::Iter iter(src, false);
SkPath::Verb verb;
SkPoint pts[4];
SkVector step;
for (;;) {
switch (verb = iter.next(pts, false)) {
case SkPath::kLine_Verb: {
bool drawSegment = ComputeStep(pts[0], pts[1], fRadius, &step);
dst->quadTo(pts[0].fX, pts[0].fY, pts[0].fX + step.fX, pts[0].fY + step.fY);
if (drawSegment) {
dst->lineTo(pts[1].fX - step.fX, pts[1].fY - step.fY);
}
break;
}
// Other verb cases omitted.
}
}
// Other iteration implementation omitted.
}
static bool ComputeStep(const SkPoint& a, const SkPoint& b, SkScalar radius, SkPoint* step) {
SkScalar dist = SkPoint::Distance(a, b);
*step = b - a;
if (dist <= radius * 2) {
*step *= SK_ScalarHalf;
return false;
} else {
*step *= radius / dist;
return true;
}
}
Usage
The implementation differences highlighted above are interesting but perhaps still a little abstract. Let’s get real. Which rounding method should you use?
My recommendation would be to consider PolygonDrawingUtil if:
- you are drawing regular polygons, and
- you prefer or need exactly controllable corner radius2, or
- you prefer or need uniform corner radius, or
- you prefer or need a simple, predictable degenerate shape.
On the other hand, consider CornerPathEffect
if:
- you are rounding an existing
Path
that’s not a regular polygon, or - you don’t care about exact corner shape or radius, or
- you prefer to or need to avoid third-party dependencies.
Either way, I hope you learned a little about Path
s, PathEffect
s, and geometry during this exploration :)
-
More useful native code if you want to go deeper: SkPath.h, SkPath.cpp, SkPathEffect.h, SkPathEffect.cpp. ↩
-
Trivia: PolygonDrawingUtil was originally inspired by games based on hexagonal grids, which is why it’s polygon-specific and allows such precise corner control. ↩