Bézier curves from first principles
I've been writing a tool that takes SVGs apart and rebuilds them, which means I spend my evenings staring at path data. Path data is mostly Bézier curves, and I realised I'd been nodding along to that name for years without knowing what the numbers meant. These are the notes I took while fixing that. The demos are draggable.
The line that started all this looks something like:
<path d="M 12 3 C 5 9 4 16 12 21" />
My parser had to eat thousands of these, and for the first week I
treated the six numbers after C as noise the browser
understood and I didn't. That worked right up until I needed to
split a path in half, and then it very much didn't. So I went and
learned the thing properly, and the annoying part is that it's
easy. The whole construction needs one operation, applied
repeatedly, and it's an operation I already used every day.
Some history first, because I fell down that hole too. In 1962 Pierre Bézier was an engineer at Renault with a problem: car bodies were designed as physical clay models, and there was no good way to describe their surfaces to the milling machines that had to cut them. The curves he popularised were actually worked out a few years earlier by Paul de Casteljau at Citroën, who wasn't allowed to publish. Same idea in both places: pin a curve down with a handful of points, and it can be reproduced exactly, anywhere, at any scale. Sixty years later that idea is load-bearing for fonts, SVG, the pen tool, CSS easing, and apparently my side project.
Start with a line
Take two points, A and B, and a
number t between 0 and 1. Linear interpolation
(lerp) gives you the point that is a fraction t of the
way from A to B:
lerp(A, B, t) = (1 − t) · A + t · B
At t = 0 you're on A, at t = 1 you're on
B, at t = 0.5 you're halfway. Sweep t from
0 to 1 and you trace the segment. I'd written this function a
hundred times for animations without ever suspecting it was the
entire toolbox. A straight line traced this way is technically
already a Bézier curve — degree 1, control points A and B —
everything below is lerps feeding into other lerps.
De Casteljau's algorithm
Now take three points: P₀, P₁ and P₂. For a given
t:
- Lerp from P₀ to P₁, call the result Q₀.
- Lerp from P₁ to P₂ with the same t, call it Q₁.
- Lerp from Q₀ to Q₁. That point is on the curve.
Sweep t and that final point traces a smooth arc: a
quadratic Bézier. The recursive walk is De
Casteljau's algorithm, and honestly, reading it in prose did
nothing for me. Watching it move did. Drag the points, scrub
t:
The detail that got me: the amber line isn't just scaffolding. At
every t it is tangent to the curve. The
construction hands you directions for free, which mattered to me
later — anything that wants to offset a path, or aim a dash along
it, needs those tangents.
One more level: the cubic
Nothing stops you at three points. With four, you lerp three times
to get three points, lerp those to get two, and lerp once more to
land on the curve. That's a cubic Bézier — and
those four points are exactly what an SVG C command
carries. The current position is P₀; the six numbers are P₁, P₂,
P₃. The line my parser choked on was four points all along. Cubics
dominate because they're the cheapest curve that can hold an S-bend,
and you get independent control over how the curve leaves each
endpoint.
Unrolling the recursion gives a closed form. Collect the terms and
each control point ends up weighted by a polynomial in
t:
B(t) = (1−t)³ P₀ + 3(1−t)²t P₁ + 3(1−t)t² P₂ + t³ P₃
Those weights are the Bernstein basis polynomials.
They're the terms of the binomial expansion of
((1−t) + t)³, so at every t they sum to
exactly 1 — the curve point is always a weighted average of the
control points. Each weight peaks at a different t,
which is the precise sense in which P₁ "pulls" the early part of the
curve and P₂ the late part. The curve passes through its endpoints
but only approaches the middle control points; it never
touches them. I sampled a lot of curves before believing that.
The derivative falls out of the algebra, and it's another Bézier curve, one degree lower:
B′(t) = 3(1−t)² (P₁−P₀) + 6(1−t)t (P₂−P₁) + 3t² (P₃−P₂)
At t = 0 that collapses to 3(P₁−P₀): the
curve leaves P₀ aimed directly at P₁. Which means a pen tool's
handles are a literal picture of the derivative at each anchor. I'd
been dragging them for years without knowing that.
Why these curves won
Plenty of curve families existed before Bézier's. As far as I can tell, this one took over because a short list of properties happens to be exactly what designers, font engineers and renderers need:
- The control points mean something. Endpoints are on the curve; handles set the tangents. You can hand the four points to a person with a mouse and they can steer the curve by feel. Try explaining a polynomial's coefficients to anyone.
- Convex hull. Because the Bernstein weights are non-negative and sum to 1, the curve can never escape the convex hull of its control points. My app leans on this constantly: you can bound, cull, or intersection-test a curve without evaluating a single point of it.
- Affine invariance. Rotate, scale or skew the control points and the curve transforms with them, exactly. You transform 4 points instead of 200 sampled ones, which is why a font scales from 8px to a billboard with no loss.
- Variation diminishing. The curve wiggles no more than its control polygon does: a line crosses the curve at most as many times as it crosses the polygon. No surprise oscillations.
- Cheap. A handful of lerps per point, in fixed or floating point, no trigonometry anywhere.
Where you've already used them today
Fonts. A glyph outline is a loop of Bézier segments.
TrueType (.ttf) uses quadratics, picked in the late '80s
because they're cheaper to rasterise; PostScript and CFF/OpenType
outlines use cubics. When this page loaded, your browser took the
curve descriptions out of three font files and flattened several
thousand of them into the pixels you're reading right now.
Vector graphics. SVG's path syntax exposes the
machinery directly: Q is a quadratic segment,
C a cubic. These two paths are figures 1 and 2 of this
page, control point for control point:
<!-- one quadratic, one cubic: figures 1 and 2 -->
<path d="M 70 300 Q 320 60 570 300" />
<path d="M 70 300 C 180 70 460 70 570 300" />
There are also two shorthand commands I only met while writing the
parser: S and T. They omit the first
control point and have the renderer reflect the previous
segment's last one through the current anchor. That reflection is
smoothness, encoded in the syntax itself — the file format is
quietly enforcing that handles stay collinear across the joint.
More on why that matters at the end.
Animation easing. CSS's
cubic-bezier(x1, y1, x2, y2) is a cubic in disguise, with
a twist: the axes aren't space, they're time (x) against
progress (y). P₀ is pinned at (0,0) and P₃ at (1,1); you only
get to place the middle two points. The browser then answers "at 40%
of the duration, how far along is the animation?" by intersecting the
curve with a vertical line. That's why the spec clamps x₁ and x₂ to
[0,1]: time isn't allowed to flow backwards. The y values are
unclamped, and pushing them outside [0,1] is how you get overshoot and
bounce.
Everything that moves along a path. Camera rails in games, GPS route smoothing, the toolpath of a 3D printer, PowerPoint's motion paths, robot arm trajectories. Any time something needs to get from A to B without a visible kink, there's a good chance a cubic Bézier is doing the steering.
How a curve becomes pixels
A GPU rasteriser wants straight lines, so at some point every Bézier
on your screen gets flattened into a polyline. My first
attempt sampled a fixed number of t values, and it looked
fine until it didn't: segments wasted on the straight-ish parts, the
tight bends visibly faceted. The standard answer uses a property of De
Casteljau's construction I did not see coming: the intermediate points
from figure 2 don't just locate one point, they are the
control points of the two halves of the curve, split at
t, and they hug the curve more tightly than the original
four.
So the renderer recurses: if a curve's control points are within
tolerance of a straight line, emit a segment; otherwise split at
t = ½ and repeat. Flat regions terminate early, bends
subdivide deeper, and the error is bounded everywhere. This same
split, at an arbitrary t, is also how you cut a curve in
two exactly — the problem that sent me down this entire rabbit hole
answers itself as a side effect of the rendering algorithm.
What they can't do
Two limitations I ran into, and what the field does about them.
No polynomial curve is a circle. A cubic can only
approximate an arc, which surprised me — circles feel like the easy
case. The classic trick pins a cubic to a quarter circle and asks how
long the handles should be; the answer is
k ≈ 0.5523 · r, a number quietly hard-coded into nearly
every graphics library that offers rounded corners. The error over a
quarter circle is about 0.02% of the radius, invisible at any screen
size.
One curve doesn't scale to complex shapes. My first instinct was: more control points. De Casteljau doesn't care about degree, so why not ten? The playground below will show you why not. Add points and try to steer the thing — every point you move bends the entire curve, and the shape drifts further and further from its control polygon. Around degree 6 it stops feeling like steering and starts feeling like negotiating.
So practice goes the other way: keep the pieces cubic and
chain them. As long as each joint's incoming and
outgoing handles are collinear, the seam is invisible — that's the
reflection rule from the S command, and it's what a pen
tool's "smooth point" enforces. Break the handles and you get a
corner. A chain of Béziers is a spline. This last playground
is the one I actually wanted while debugging: a pen-tool spline that
prints its own path data as you drag, so you can watch the geometry
and the d string move together.
Add per-point weights to a spline so arcs come out exact and you get NURBS, the standard in CAD and a direct descendant of what Bézier and de Casteljau built for car bodies. That's where I stopped reading; the app doesn't need it. Yet.
Further reading
Things I leaned on while figuring this out:
- A Primer on Bézier Curves, Pomax's free book. The reference for everything this post skipped (arc length, offsetting, intersections).
- The Beauty of Bézier Curves, Freya Holmér's 24-minute video. The best visual treatment of splines and continuity I've found anywhere.
- Raph Levien's curve notes, for when the practical questions (arc length, flattening error bounds) get serious.