rishikesh.s / notes

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:

t = 0.400
Fig. 1 · the two grey lerps produce the amber points; the amber lerp produces the point on the curve. Drag any control point; handles also move with arrow keys when focused.

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.

t = 0.350
Fig. 2 · a cubic is three nested layers of lerp. Try dragging P₁ across the line from P₀ to P₃ to put an S-bend in it.

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.

cubic-bezier(.22, .70, .20, 1)
Fig. 3 · drag the two handles, then run to feel the easing. The default is this site's own transition curve, --ease in its stylesheet. Drag a handle's y outside the plot to make the dot overshoot.

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.

12 segments
Fig. 4 · adaptive subdivision. Tighten the tolerance and segments appear exactly where the curvature is; drag the points and watch the subdivision redistribute itself.

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.

Fig. 5 · one cubic (solid) laid over a true circle (faint). Each handle is tangent to the circle with length k·r, k ≈ 0.5523. Four of these make the "circles" in most icon sets.

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.

degree 4 · t = .45
Fig. 6 · the whole De Casteljau pyramid, at any degree up to 7. Each amber layer is one round of lerps. Add points and notice how little the curve respects them.

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.

Fig. 7 · two cubics sharing the middle anchor. With smooth joint on, dragging one middle handle reflects the other, exactly like a pen tool — and exactly like the S command. Turn it off and drag one to make a corner. The d string below the canvas is live; paste it into any SVG.

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:

← All writing