Making Corners Round: The Four-Point Circle
Corner radius. Seems simple: round the corners of a shape. Every design tool has this.
We spent three weeks on it.
The Problem
Figma lets you set a corner radius on vector network vertices. A rectangle with 20px corner radius should have smooth, circular-looking rounded corners.
Draw it. The corners were lumpy. Visibly non-circular.
First Attempt: Simple Arc Insertion
Let's add circular arcs at the corners:
void roundCorner(uint32_t vertexId, float radius) {
// Get two segments meeting at this vertex
auto [seg1, seg2] = getSegmentsAtVertex(vertexId);
// Calculate arc sweep angle
float angle = angleBetweenSegments(seg1, seg2);
// Insert circular arc
addArcSegment(seg1.endpoint, seg2.startpoint, radius, angle);
}
The problem: this creates an actual circular arc—a primitive Skia doesn't directly support in paths. Paths have moves, lines, and cubic beziers. Not arcs.
We'd have to convert the arc to bezier curves. How many bezier segments? Arc tessellation is its own rabbit hole.
Second Attempt: One-Third Distance Method
Someone suggested: "Just place the bezier control points one-third of the distance toward the corner."
SkPoint cutPoint1 = calculateCutPoint(seg1, radius);
SkPoint cutPoint2 = calculateCutPoint(seg2, radius);
SkVector toCorner1 = cornerVertex - cutPoint1;
SkVector toCorner2 = cornerVertex - cutPoint2;
// Control points at 1/3 distance
tangent1 = toCorner1 * 0.333f;
tangent2 = toCorner2 * 0.333f;
This produced... something. It was round-ish. But not circular. The approximation error was ~8%. You could see it.
For a 20px radius corner, the actual curve was off by 1.6 pixels. Visible at any reasonable zoom level.
The Math: Why 0.5522847 Isn't Magic
This is a solved problem in computer graphics, but most explanations just give you the number without explaining where it comes from.
Here's the intuition:
A cubic bezier curve is defined by four points: two endpoints and two control points. For a 90° circular arc from (R, 0) to (0, R), the endpoints are fixed. The question is: where do the control points go?
If you place them too close to the endpoints, the curve bows inward—it's too flat. Too far away, it bulges outward. There's exactly one distance that makes the curve match a circle.
That distance is: R × (4×(√2-1))/3
Let's calculate:
√2 ≈ 1.41421356 (diagonal of a unit square)
√2 - 1 ≈ 0.41421356
4 × 0.41421356 ≈ 1.65685424
1.65685424 / 3 ≈ 0.55228474
The constant: 0.5522847
Not 0.333 (one-third). Not 0.5 (halfway). Not an approximation rounded to something convenient. This exact value.
Where It Comes From
The derivation requires solving the bezier curve equation at t=0.5 (midpoint) and setting it equal to the circle's radius:
For a unit circle centered at (1,1), the 90° arc from (1,0) to (0,1):
- Circle midpoint: distance from origin = 1 (by definition)
- Bezier midpoint: must also have distance 1 from origin
The bezier curve formula at t=0.5:
P(0.5) = 0.125×P₀ + 0.375×P₁ + 0.375×P₂ + 0.125×P₃
Solving |P(0.5)| = 1 gives control point offset k = (4/3)×tan(π/8) = 0.5522847...
This is the unique value that makes the bezier curve pass through the circle at its midpoint. For a 90° arc, this also happens to minimize the maximum radial error across the entire curve (< 0.027%).
Why Everyone Uses This Same Number
You'll find 0.5522847 (or slight variations like 0.552284749831) in:
- SVG implementations (Firefox, Chrome, Safari)
- PostScript and PDF rendering
- Font renderers (FreeType, CoreText)
- CSS border-radius under the hood
- Design tools (Figma, Sketch, Illustrator)
It's not magic. It's the mathematically optimal solution to "approximate a 90° circular arc with a cubic bezier."
The Solution: Four-Step Algorithm
We needed to transform the topology, not just modify geometry.
Step 1: Detect corners (which vertices have corner radius > 0)
Step 2: Calculate cut points where the rounded corner starts:
// Bisector method
float tDistance = radius / tan(angle / 2);
// For bezier segments, use tangent direction, not vertex direction
SkVector direction = seg.hasCurve
? seg.tangentAtVertex
: (seg.endPoint - seg.startPoint);
SkPoint cutPoint = vertex + direction.normalized() * tDistance;
Create virtual vertices at these cut points.
Step 3: Shorten segments by changing their endpoints to the cut point vertices. Preserve original tangents—if it was a bezier, keep the bezier shape, just trim it shorter.
Step 4: Insert corner segment between the two cut points:
// Vector from cut point toward corner
SkPoint toCorner1 = cornerVertex - cutPoint1;
SkPoint toCorner2 = cornerVertex - cutPoint2;
// Apply the bezier circle constant
constexpr float k = 0.5522847290039062f;
cornerTangents.from = toCorner1 * k;
cornerTangents.to = toCorner2 * k;
addSegment(cutPoint1, cutPoint2, cornerTangents);
The tangents point toward the corner vertex, scaled by the magic constant. This creates a bezier curve that closely approximates a circular arc.
Why This Works
The constant 4×(√2-1)/3 comes from solving for the control point positions that minimize radial error.
For a unit circle (radius 1), a 90° arc from (1,0) to (0,1):
- Arc midpoint should be at distance 1 from origin
- Bezier midpoint (at t=0.5) needs to match
The math works out to requiring control points at:
P1 + (0, k)wherek = (4/3) × tan(π/8) = 0.5522847...
This isn't magic—it's derived from the bezier curve formula and circle equation. But the constant is specific to 90° arcs.
For other angles, the approximation quality degrades slightly. For typical UI corner angles (60°-120°), the error is still <0.3%, which is imperceptible.
The Topology Transformation
The key insight: corner radius adds vertices and segments. It's not just modifying existing geometry.
Before (src/cf/CfVectorNetworkCornerRadius.cpp:769):
Vertex 0 ──────── Vertex 1
│
│
Vertex 2
After:
Vertex 0 ──── CutPoint1 ╭─ Corner ─╮ CutPoint2 ──── Vertex 2
Virtual segment
The corner vertex (1) now connects to two new virtual vertices (cut points) instead of the original segments. The virtual segment between cut points has bezier tangents calculated with the constant.
Results
Corner radius test cases (CORNER_RADIUS_FINAL.md:49-90):
90° corners: Visually indistinguishable from circles 120° corners: <0.3% radial error Mixed straight/bezier: Works correctly, preserves original curve shapes Multiple corners: All rounded independently
The formula R × 0.5522847 replaced hundreds of lines of arc tessellation code. And the approximation is better.
What We Learned
The "one-third" rule is a common approximation, but it's wrong for circles. The correct value is 4×(√2-1)/3 ≈ 0.5522847.
This constant appears everywhere in graphics:
- SVG path implementations
- Font rendering (TrueType curves)
- PostScript arc approximations
- CSS border-radius rendering
It's not magic. It's math that someone solved decades ago. We just had to find it.
Corner radius isn't about drawing arcs. It's about topology transformation: detecting corners, creating virtual vertices, shortening segments, and inserting bezier curves with correctly calculated tangents.
Four steps. One constant. Three weeks of trial and error to figure out we needed both.
Read next: Stroke Joins: The Devil in the Geometry - Connecting dashed segments at corners without gaps or overlaps.