Computer Graphics

Bezier Curves

Smooth curves you steer with a handful of points

A Bezier curve is a smooth parametric curve traced by repeatedly interpolating between control points. The de Casteljau algorithm evaluates it in O(n²) per point; the Bernstein form gives the same curve as a weighted polynomial.

  • Evaluate one point (de Casteljau)O(n²)
  • Evaluate one point (Bernstein)O(n)
  • Control points (cubic)4
  • Endpoints touchedfirst & last only
  • Circle drawable exactly?No (use NURBS)

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

How a Bezier curve is built from lerps

Take four points and ask: what is the smoothest way to travel from the first to the last that "leans toward" the middle two? Pierre Bézier, an engineer at Renault, formalized the answer in the 1960s so car-body designers could shape panels by dragging a few handles. The same math now draws every font glyph, SVG path, CSS animation curve, and vector illustration on your screen.

The mechanism is just linear interpolation, applied recursively. Given control points P₀, P₁, P₂, P₃ and a parameter t ∈ [0, 1], the de Casteljau algorithm does this:

  1. Lerp each adjacent pair at t: A = lerp(P₀,P₁,t), B = lerp(P₁,P₂,t), C = lerp(P₂,P₃,t). Four points become three.
  2. Lerp those: D = lerp(A,B,t), E = lerp(B,C,t). Three become two.
  3. Lerp once more: point = lerp(D,E,t). The single survivor is the curve point at t.

Sweep t from 0 to 1 and the survivor traces the curve. Because each layer is a straight-line blend, the result is a polynomial of degree n for n+1 control points — degree 3 for the cubic above. The curve passes through P₀ and P₃ exactly (at t=0 and t=1) but only leans toward P₁ and P₂. That gap between "control" and "touch" is the whole user-experience win: a designer can shape the bulge and tangent without the curve snapping onto the handle.

The Bernstein form: the same curve, one line

De Casteljau's recursion is intuitive but it is not the cheapest way to evaluate a single point. Expand the algebra and the nested lerps collapse into a weighted sum of the control points:

B(t) = Σ  C(n,i) · tⁱ · (1−t)ⁿ⁻ⁱ · Pᵢ
       i=0..n

The weights C(n,i)·tⁱ·(1−t)ⁿ⁻ⁱ are the Bernstein basis polynomials. For a cubic this is the familiar formula every graphics programmer memorizes:

B(t) = (1−t)³·P₀ + 3(1−t)²t·P₁ + 3(1−t)t²·P₂ + t³·P₃

Two facts fall out for free. First, the four weights always sum to 1 — the curve is an affine combination of its control points, which is why rotating or translating the control points rotates or translates the curve identically. Second, every weight is non-negative on [0,1], so the curve never leaves the convex hull of its control points — a property renderers exploit to cull and clip curves cheaply.

When to reach for a Bezier curve

  • Vector graphics and fonts. SVG, PostScript, PDF, and OpenType outlines are all chains of Bezier segments. It is the lingua franca of resolution-independent shapes.
  • Animation easing. CSS cubic-bezier(.25,.1,.25,1) maps time to progress; the same curve drives keyframe interpolation in After Effects, Lottie, and game engines.
  • Interactive design. The pen tool in Illustrator, Figma, and Inkscape is a Bezier editor — anchor points plus draggable handles.
  • Motion paths and camera rigs. A cubic gives an artist a smooth path with editable tangents without solving a global spline system.

Reach for something else when you need a curve that passes through every data point (use an interpolating spline like Catmull-Rom), when you need an exact circle or ellipse (use NURBS), or when you need a single curve with dozens of independently-editable wiggles (use a B-spline, which keeps each control point's influence local).

Bezier vs other curve representations

BezierB-splineNURBSCatmull-RomHermite
Passes through interior points?NoNoNoYesYes (endpoints + tangents)
Control point influenceGlobalLocalLocalLocalLocal
Can draw an exact circle?NoNoYes (rational)NoNo
Specified byControl pointsControl pts + knotsControl pts + knots + weightsPoints the curve hitsEndpoints + tangents
Evaluate one pointO(n) / O(n²)O(degree)O(degree)O(1)O(1)
Typical useFonts, SVG, easingCAD surfacesCAD, exact conicsGame pathsPhysics, keyframes

The defining trade-off is locality versus simplicity. A single Bezier is dead simple but every control point reshapes the whole curve, so practical paths chain many low-degree Bezier segments. B-splines bake that chaining into one curve with local control; NURBS add per-point weights so the same machinery can represent exact circles and ellipses. A Bezier is in fact just a B-spline with no interior knots, and a non-rational NURBS with all weights equal to 1 — they are points on the same family tree.

What the numbers actually say

  • De Casteljau is O(n²); Bernstein is O(n). For a cubic that is 6 lerps (each 2–3 multiplies) versus a 4-term Horner evaluation. De Casteljau costs roughly 2–3× more per point — you pay it for numerical stability and for free curve splitting.
  • The magic circle constant is 0.5523. Four cubic arcs with off-curve handles at k·r where k = 4(√2 − 1)/3 ≈ 0.5523 approximate a circle with a maximum radial error of about 0.00027·r — under three ten-thousandths of the radius, invisible at any screen size.
  • Adaptive subdivision resolves a glyph in 4–8 splits per curve. Recursively split until the control points lie within ~0.25 px of the chord; typical text rendering hits that flatness bound in a handful of subdivisions, far fewer than the dozens of fixed-step samples a naive renderer would use.
  • Cubic is the universal cap. SVG, PostScript, and CFF/OpenType outlines max out at cubic (4 points); TrueType uses quadratic (3 points). Higher degree buys almost no expressive power because designers chain segments instead.

JavaScript implementation

The de Casteljau evaluator works for any degree and any dimension — it just blends arrays of numbers:

const lerp = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t);

// De Casteljau: evaluate a Bezier of any degree at parameter t.
// points = array of [x, y] (or [x, y, z]); returns one point.
function deCasteljau(points, t) {
  let pts = points;
  while (pts.length > 1) {
    const next = [];
    for (let i = 0; i < pts.length - 1; i++) {
      next.push(lerp(pts[i], pts[i + 1], t));
    }
    pts = next;            // each pass shrinks the array by one
  }
  return pts[0];
}

// Closed-form cubic via the Bernstein basis — faster, fixed degree.
function cubicBezier(P0, P1, P2, P3, t) {
  const u = 1 - t, uu = u * u, tt = t * t;
  const w0 = uu * u, w1 = 3 * uu * t, w2 = 3 * u * tt, w3 = tt * t;
  return P0.map((_, i) =>
    w0 * P0[i] + w1 * P1[i] + w2 * P2[i] + w3 * P3[i]);
}

// Sample the curve into a polyline for rendering.
const ctrl = [[0, 0], [1, 3], [3, 3], [4, 0]];
const path = Array.from({ length: 41 }, (_, k) =>
  deCasteljau(ctrl, k / 40));

Two things are worth noticing. deCasteljau is degree- and dimension-agnostic because it only ever does element-wise lerps; pass three-element points and you get a 3D space curve with no code changes. The cubic special case avoids the inner loop entirely, which is why hot paths (font rasterizers, CSS engines) hard-code it.

Python implementation

The same two evaluators, plus the de Casteljau split that the closed form cannot give you for free:

def lerp(a, b, t):
    return tuple(x + (y - x) * t for x, y in zip(a, b))

def de_casteljau(points, t):
    pts = list(points)
    while len(pts) > 1:
        pts = [lerp(pts[i], pts[i + 1], t) for i in range(len(pts) - 1)]
    return pts[0]

def cubic_bezier(p0, p1, p2, p3, t):
    u = 1 - t
    w0, w1, w2, w3 = u*u*u, 3*u*u*t, 3*u*t*t, t*t*t
    return tuple(w0*a + w1*b + w2*c + w3*d
                 for a, b, c, d in zip(p0, p1, p2, p3))

def split(points, t):
    """Split one Bezier into two at t. The intermediate lerp
    results ARE the new control polygons — a de Casteljau freebie."""
    left, right, pts = [], [], list(points)
    while pts:
        left.append(pts[0])
        right.insert(0, pts[-1])
        if len(pts) == 1:
            break
        pts = [lerp(pts[i], pts[i + 1], t) for i in range(len(pts) - 1)]
    return left, right

ctrl = [(0, 0), (1, 3), (3, 3), (4, 0)]
mid  = de_casteljau(ctrl, 0.5)
a, b = split(ctrl, 0.5)   # two cubics that trace the same shape

split is the famous side benefit of de Casteljau: the corner control points of every intermediate triangle are exactly the control points of the two sub-curves. That is what powers adaptive rendering, curve trimming, and intersection tests — none of which the Bernstein form gives you without re-deriving the algebra.

Variants worth knowing

Quadratic Bezier. Three points, degree 2: (1−t)²P₀ + 2(1−t)t·P₁ + t²P₂. It is a parabola arc and cannot make an S-shape (no inflection). TrueType fonts use quadratics because they render with one fewer multiply per point.

Rational Bezier and NURBS. Attach a weight wᵢ to each control point and divide by the weighted sum. Higher weight pulls the curve harder toward that point. Crucially this lets you bend a parabola into any conic, so rational curves can draw exact circles, ellipses, and hyperbolas — which polynomial Beziers never can.

Composite paths (poly-Bezier). Real shapes chain many cubic segments end to end. Sharing endpoints gives C0 continuity; collinear handles give G1; equal-length collinear handles give C1. SVG's C and S path commands are exactly this chaining, with S auto-mirroring the previous handle for smoothness.

Bezier surfaces (tensor product). A grid of control points blended by Bernstein bases in two parameters (u, v). The Utah teapot is a famous Bezier-patch model. Surfaces inherit the convex-hull and affine-invariance properties of the 1D curve.

Bezier clipping. Use the convex-hull property to find ray–curve and curve–curve intersections by repeatedly clipping away the parameter range that provably cannot contain a root. It converges quadratically and is a staple of vector renderers and CAD kernels.

Common bugs and edge cases

  • Confusing arc length with parameter. Equal steps in t do not give equal distances along the curve — the curve moves faster where control points are spread out. Moving an object at constant speed needs arc-length reparameterization, not t += dt.
  • Expecting the curve to hit the middle handles. It never touches P₁ and P₂; only the endpoints. Beginners place handles where they want the curve to pass and are surprised when it falls short.
  • Self-intersections and cusps. A cubic with badly crossed handles can loop on itself or form a cusp. Stroking such a curve produces rendering artifacts; many engines detect and split at the cusp.
  • Trying to draw a circle with one curve. A single cubic cannot, and even four arcs are an approximation. Use the 0.5523 handle length, or switch to rational curves if exactness matters.
  • Fixed-step sampling for rendering. A constant number of samples under-resolves sharp turns (faceted curve) and wastes work on near-straight runs. Use adaptive flatness-based subdivision instead.
  • Degree elevation surprises. Raising a curve's degree without changing its shape is legal and sometimes needed to match two curves' degrees before lofting — but naively adding a control point changes the shape unless you use the proper degree-elevation formula.

Frequently asked questions

Why do Bezier curves use control points instead of points the curve passes through?

A Bezier curve only touches its first and last control points; the interior ones act as magnets that pull the curve toward them without being hit. Designers prefer this because dragging a handle gives smooth, predictable, local-feeling control of the tangent and bulge, whereas a curve forced through every point (interpolating spline) can overshoot and wobble between them.

What is the de Casteljau algorithm and is it the same as the Bernstein formula?

They produce the identical curve. The Bernstein form is a direct polynomial: a weighted sum of the control points with Bernstein basis weights. De Casteljau computes the same point by repeated linear interpolation — lerp each adjacent pair at parameter t, then lerp the results, until one point remains. De Casteljau is slower (O(n²) vs O(n) per point) but numerically stable and it also splits the curve for free.

Why is a circle impossible to draw exactly with Bezier curves?

Bezier curves are polynomials, and a circle is not a polynomial — it is x² + y² = r². No polynomial of any degree traces a true circle. The standard fix is four cubic arcs with control-handle length 0.5523 × r, which approximates a quarter circle to within about 0.0003 of the radius. For an exact circle you need rational (NURBS) curves, which add weights to bend a parabola into a conic.

Why do font and SVG formats cap Bezier curves at cubic degree?

Cubics (four control points) are the sweet spot: they can form an S-shape with a single inflection, which lower degrees cannot, while staying cheap to evaluate and easy to split. Higher degrees add little expressive power but make the curve global — moving one control point reshapes the whole span — and slow to evaluate. SVG, PostScript, and most fonts use cubics; TrueType uses quadratics for even cheaper rendering.

How do you join Bezier curves smoothly into a path?

Chain segments where the last point of one is the first point of the next. For C0 continuity they just share that point. For G1 (tangent) continuity the incoming and outgoing handles must be collinear with the shared point. For C1 continuity they must additionally be the same length. Vector editors enforce this by mirroring handles when you mark an anchor as smooth.

How are Bezier curves rendered to pixels efficiently?

Naively you sample t at fixed steps, but that under-samples sharp bends and over-samples straight runs. Production renderers use adaptive subdivision: split the curve with de Casteljau and stop recursing once a segment is flat enough that the control points lie within a fraction of a pixel of the chord. This typically resolves a glyph outline in 4–8 subdivisions per curve.