Four Steps to Round Corners: Topology Transformation
You have a sharp corner. Apply "corner radius: 20px".
The corner doesn't round. It disappears.
Not a rendering bug. A topology bug. Corner radius isn't about drawing—it's about changing the graph structure itself.
The Problem
Vector networks store geometry as vertices and segments:
Vertex 0: (0, 0)
Vertex 1: (100, 0)
Vertex 2: (100, 100)
Segment A: 0→1 (horizontal)
Segment B: 1→2 (vertical)
Sharp 90° corner at vertex 1.
Apply corner radius. What happens?
The corner should become a smooth arc connecting segments A and B. But where do the arc points go? You can't just draw a curve—you need to add vertices and segments to represent it.
First Attempt: Direct Arc Insertion
Try inserting an arc segment at the corner:
void addCornerRadius(uint32_t vertexId, float radius) {
// Find segments meeting at this vertex
Segment& seg1 = findSegment1(vertexId);
Segment& seg2 = findSegment2(vertexId);
// Create arc connecting them
uint32_t arcId = addArcSegment(seg1.toVertex, seg2.fromVertex, radius);
}
Run it. The corner vertex still exists. Segments A and B still end at the sharp corner, then an arc segment also starts there.
Result: sharp corner + arc segment stacked on top = visual mess.
The arc needs cut points—the segments have to be shortened so the arc connects them smoothly.
Second Attempt: Shorten Segments Manually
Calculate where to cut the segments:
void addCornerRadius(uint32_t vertexId, float radius) {
// Calculate cut distance based on angle
float angle = calculateAngle(seg1, seg2, vertexId);
float cutDist = radius / tan(angle / 2);
// Move segment endpoints
seg1.toVertex = createVertexAt(seg1, cutDist);
seg2.fromVertex = createVertexAt(seg2, cutDist);
// Add arc between new endpoints
addArcSegment(seg1.toVertex, seg2.fromVertex, radius);
}
This creates the cut points. But those cut point locations aren't vertices yet. We reference vertex IDs that don't exist.
Crash.
You need to actually create the vertices in the vertex array before you can reference them in segments.
This is topology transformation: you're modifying the graph structure, not just drawing.
The Solution: Four-Pass Algorithm
Process corners in stages, maintaining valid topology throughout:
Pass 1: Detect Corners
Identify which vertices should have corner radius applied:
for (uint32_t vertexId = 0; vertexId < vertices.size(); vertexId++) {
if (vertexInfo[vertexId].cornerRadius > 0) {
// Find two segments meeting at this vertex
auto [seg1, seg2] = findAdjacentSegments(vertexId);
float angle = calculateAngle(seg1, seg2, vertexId);
// Skip near-straight or near-180° corners
if (angle < 0.01 || (π - angle) < 0.01) continue;
cornerInfo.isValid[vertexId] = true;
}
}
Pass 2: Calculate Cut Points and Create Virtual Vertices
For each valid corner, calculate where segments should be shortened and create vertices:
for (uint32_t vertexId : validCorners) {
float radius = vertexInfo[vertexId].cornerRadius;
float angle = cornerInfo.angle[vertexId];
// Distance from corner to cut point
float cutDistance = radius / tan(angle / 2);
// Create two virtual vertices (cut points on each segment)
uint32_t cutVertex1 = createVirtualVertex(seg1, cutDistance);
uint32_t cutVertex2 = createVirtualVertex(seg2, cutDistance);
cornerInfo.cutVertex1[vertexId] = cutVertex1;
cornerInfo.cutVertex2[vertexId] = cutVertex2;
}
These "virtual vertices" are real entries in the vertex array—the term just means they're algorithmically generated, not part of the original input data.
Pass 3: Shorten Segments
Now that cut point vertices exist, update segment endpoints:
for (uint32_t vertexId : validCorners) {
auto& seg1 = segments[cornerInfo.segment1[vertexId]];
auto& seg2 = segments[cornerInfo.segment2[vertexId]];
// Change endpoints to cut point vertices
if (seg1.toVertex == vertexId) {
seg1.toVertex = cornerInfo.cutVertex1[vertexId];
} else {
seg1.fromVertex = cornerInfo.cutVertex1[vertexId];
}
if (seg2.toVertex == vertexId) {
seg2.toVertex = cornerInfo.cutVertex2[vertexId];
} else {
seg2.fromVertex = cornerInfo.cutVertex2[vertexId];
}
}
Segments now end at the cut points instead of the original corner.
Pass 4: Insert Bezier Arcs
Connect the cut points with a smooth curve using the bezier constant:
for (uint32_t vertexId : validCorners) {
SkPoint cutPos1 = vertices[cornerInfo.cutVertex1[vertexId]];
SkPoint cutPos2 = vertices[cornerInfo.cutVertex2[vertexId]];
SkPoint cornerPos = vertices[vertexId];
// Vector from cut point toward corner
SkVector toCorner1 = cornerPos - cutPos1;
SkVector toCorner2 = cornerPos - cutPos2;
// Magic constant for circular arc approximation
constexpr float k = 0.5522847290039062f;
// Bezier tangents
SkVector tangent1 = toCorner1 * k;
SkVector tangent2 = toCorner2 * k;
// Create arc segment connecting cut points
uint32_t arcSegId = addSegment(
cornerInfo.cutVertex1[vertexId],
cornerInfo.cutVertex2[vertexId]
);
setSegmentTangents(arcSegId, tangent1, tangent2);
}
The constant 0.5522847290039062 comes from the formula for approximating a circular arc with a cubic bezier: R × (4 × (√2 - 1)) / 3.
For a 90° arc, this produces a curve that visually matches a perfect circle to within 0.02% error.
The Bezier Circle Constant
Why that specific number?
A circular arc can be approximated by a cubic bezier curve where the control points are offset from the endpoints by a specific fraction of the radius.
For a quarter circle (90°):
- Arc starts at (r, 0)
- Arc ends at (0, r)
- Control points at (r, r×k) and (r×k, r)
The optimal value of k that minimizes error:
k = 4 × (√2 - 1) / 3 ≈ 0.5522847290039062
This isn't arbitrary—it's derived from the geometry of circular arcs and bezier curves. The formula balances curvature error across the entire arc length.
Results
Corner radius implementation now:
- Creates 2 virtual vertices per corner (cut points)
- Shortens 2 segments per corner
- Adds 1 bezier arc segment per corner
- Maintains valid topology throughout
For a rectangle with 4 rounded corners:
- 8 virtual vertices created
- 4 original segments shortened
- 4 arc segments added
- Total: 8 segments forming smooth rounded rectangle
The algorithm handles:
- Straight-to-straight corners (like rectangles)
- Bezier-to-bezier corners
- Mixed straight-to-bezier corners
- Variable angles (not just 90°)
For non-90° corners, the constant k is applied to the vector toward the corner, which automatically adapts to the angle. It's less accurate for very sharp (< 60°) or very wide (> 120°) angles, but produces good results for typical corner cases.
The four-pass structure ensures topology stays valid: vertices exist before segments reference them, segments are updated before arcs connect them.
Corner radius isn't rendering. It's graph surgery.
Read next: Figma's Blend Modes: Not Quite Photoshop - Manual shader implementation for divergent blend formulas.