Straight lines break at corners. Bezier curves don't.
At the end of the last chapter, our line had a problem. When it changed direction — say, going from horizontal to diagonal — the segments met in a sharp angle. It looked like a broken stick. Real transit lines flow. They curve. This chapter is about that curve.
The tool we need is the cubic bezier curve. It's one of SVG's built-in path commands, and it's the workhorse of every smooth line you've ever seen on a screen — from font outlines to animation paths to, yes, transit maps.
A cubic bezier is defined by four points: a start, an end, and two control points that shape the curve between them. The curve doesn't pass through the control points — instead, it's pulled toward them, like gravity. Move a control point and the curve reshapes itself.
That's abstract. Let's make it concrete. Drag the green control points in the interactive below and watch the curve respond:
d="M 80 200 C 200 60, 420 260, 540 100"
Play with it. Drag both control points to the same side and watch the curve bow outward. Stack them vertically and see an S-curve form. Pull them far away and the curve stretches dramatically. Bring them close to the straight line between start and end — the curve flattens.
The SVG syntax for this is the C command inside a <path>:
d="M 80 200 C 200 60, 420 260, 540 100"
Breaking that down: M 80 200 moves to the start point. C starts a cubic bezier with three coordinate pairs — control point 1 (200, 60), control point 2 (420, 260), and the end point (540, 100). That's the entire syntax.
The Intuition
Think of the control points as magnets. The curve leaves the start point heading toward control point 1, and arrives at the end point coming from control point 2. The further away a control point is, the stronger its pull. This "heading toward / coming from" behavior is why beziers are perfect for transit lines — you control the direction the line leaves and arrives at each station.
From Playground to Transit
How does this apply to our metro line? Remember the problem: Station A is at the left, Station C is up and to the right, and the track bends at Station B in between. With straight segments, the bend is a sharp angle.
The fix: instead of two straight segments (A→B and B→C), draw one bezier curve from A to C that passes through B's vicinity. Or better: draw two bezier segments (A→B and B→C), each with control points that ensure they meet smoothly at B.
For the control points to create a smooth join at B, they need to be collinear — the outgoing control point of the first segment and the incoming control point of the second segment should lie on the same line through B. This ensures the curve doesn't kink.
Live Editor — Sharp angles vs smooth beziers
The dashed pink line shows the straight segments. The solid amber line shows the bezier version. Same stations, same data — the only difference is how we connect the dots.
The key variable is smoothing — currently set to 0.3. This controls how far the control points extend from each station, as a fraction of the distance to the next station. Try changing it:
0.0 — no smoothing, identical to straight segments. 0.2 — subtle rounding at corners. 0.3 — the sweet spot for most transit maps. 0.5 — dramatic curves, the line starts to bulge. 0.8 — too much, the curves overshoot and look wobbly.
The Smoothing Algorithm
Here's what's happening at each station: we compute the average angle between the incoming segment and the outgoing segment. The control point extends along this averaged direction by smoothing × distance_to_next_station. Because both sides of the junction use the same averaged angle, the curve passes through the station smoothly — no kink.
At terminus stations (start/end of line), there's no averaging needed — the control point just extends along the direction toward the neighboring station.
The Smoothing Function
Let's extract the smoothing logic into a reusable function. This is the function we'll use for the rest of the guide:
Live Editor — Reusable smoothPath() function
Three lines, all using the same smoothPath() function. The amber line bends upward, the pink one dips and recovers (an S-curve through four stations), the blue one makes a dramatic diagonal dive. All smooth. All from the same 30-line function.
Try editing the point coordinates. Move the blue line's third point from {x:350, y:180} to {x:350, y:50} — watch it flatten. Move the pink line's middle point to {x:300, y:50} — watch the S-curve become a mountain.
When to Smooth, When to Keep Straight
Not every segment needs a bezier. Two stations at the same y-coordinate? A straight line is cleaner. The smoothing adds visual noise where none is needed.
A practical heuristic: if the angle between two consecutive segments is less than about 10°, use a straight line. The human eye can't see the difference at small angles, and straight segments are simpler and faster to render.
Live Editor — Adaptive: curves only where needed
Five out of six segments are straight — only the Central→Hillside transition triggers a bezier, because that's the only place where the direction changes significantly. The result is clean: straight where it should be straight, curved where it needs to curve.
This is exactly how professional transit maps handle it. Look at any system's official map and you'll see long straight runs punctuated by smooth transitions. The curves exist to serve the geometry, not to show off.
The Function You'll Keep
The smoothPath() function from this chapter is a keeper. It takes an array of points and returns an SVG path string with adaptive bezier smoothing. We'll use it in every subsequent chapter. Here's what it does in plain language:
1. Start at the first point. 2. For each segment, check if the direction change at either end exceeds ~10°. 3. If no: draw a straight line (L command). 4. If yes: compute control points by extending along the averaged direction at each station, at a distance of smoothing × segment_length. Draw a cubic bezier (C command). 5. The averaged direction at each station ensures adjacent curves meet without kinking.
The Discovery
We initially wrote a much more complex smoothing algorithm that used Catmull-Rom spline conversion, tension parameters, and special cases for collinear points. It produced slightly better curves in edge cases, but the code was five times longer and impossible to debug visually.
The smoothing = 0.3 one-parameter version handles 95% of transit geometries. The remaining 5% are weird edge cases (hairpin turns, switchbacks) that don't appear in real metro networks because trains can't make those turns either.
Simple wins.
We now have everything we need to draw a single line beautifully: stations with positions, smooth paths between them, adaptive labels, and terminus markers. In the next chapter, we face the real challenge — what happens when a second line shows up and wants to share the same stations.