Nick Butcher recently demonstrated a
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 dynamically generates and draws rounded polygon
Paths based on user-specified attributes:
- polygon side count,
- polygon center coordinates,
- polygon radius (center to corner), and
- desired corner radius.
Paths 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
Paths 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.arcToto 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.
PathEffects are used to modify how an existing
Path is drawn. Applied via
Paint.setPathEffect, they grant
Paints 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
CornerPathEffect draws rounded polygon
Paths 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.
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
- you are rounding an existing
Paththat’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
PathEffects, and geometry during this exploration :)