Skip to content

MathematicsInterpolationcommon4 min read

Bézier Curves

lerp between lerps until one point is left

A Bézier curve is nested lerps between control points. Learn quadratic and cubic forms, tangents, and arc-length tables for constant-speed motion.

View .md

The white line is the cubic curve; its four white control points drift slowly and the grey dashed line is the control polygon. The grey lines are the de Casteljau construction for the current t, ending at the grey ring (naive t) with its yellow dashed tangent. The solid yellow ball moves at constant speed using the arc-length table. Tick marks show equal-t or equal-distance spacing.

What it is

A Bézier curve is defined by control points. It starts at the first, ends at the last, and is pulled toward the ones in between without usually touching them. A quadratic curve has three points (one handle); a cubic has four (two handles) and is the workhorse for camera rails, projectile arcs, UI motion paths, road splines and font outlines.

de Casteljau's construction shows what is going on. For a parameter t, lerp each neighbouring pair of control points by t, then lerp those results, and repeat until one point is left. That point is on the curve. The last segment of the construction is the tangent at that point, so you get the direction for free. Expanding the nested lerps gives the Bernstein polynomial form, which is cheaper to evaluate in a tight loop.

`t` is not distance. Equal steps in t bunch up where the control points are close and spread out where they are far apart, so an object moving with t = time / duration speeds up and slows down. For constant speed, sample the curve once into an arc-length table (cumulative chord lengths at evenly spaced t), then turn a distance travelled into t with a binary search. See also easing for deliberately non-uniform speed.

To chain cubics into a longer path without a kink, make the handles on either side of a joint collinear with it (G1), or mirror them at equal length (C1, smooth velocity). Three.js ships QuadraticBezierCurve3, CubicBezierCurve3 and CurvePath, with getPointAt(u) already doing the arc-length lookup.

  • bezier
  • curve
  • spline
  • de casteljau
  • arc length
  • path

Key formulas

Quadratic
Cubic (Bernstein form)

The white curve in the visual.

de Casteljau step

Each grey construction level has one point fewer; the last one is B(t).

Cubic tangent
Arc length

Store s at N evenly spaced t, then invert it with a binary search.

Rules of thumb

  • 64–256 table samples is plenty for game paths; rebuild the table only when control points change.
  • Constant speed: distance += speed * dt, then t = tAtDistance(table, distance).
  • A cubic's start tangent is 3·(P1 − P0): aim the first handle where the motion should leave from.
  • Smooth joints: next curve's P1 = 2·P3 − P2 of the previous curve (C1).
  • Keep handles to about ⅓ of the segment length for curves that look natural.

Implementation

Constant-speed follower with Three.js (it builds the arc-length table for you):
TypeScript
const path = new THREE.CubicBezierCurve3(  new THREE.Vector3(-4, 0, 1.5),  new THREE.Vector3(-3.6, 0, -1.2),  new THREE.Vector3(3.5, 0, -3),  new THREE.Vector3(4, 0, 2),)path.arcLengthDivisions = 200 // table resolutionconst length = path.getLength()const pos = new THREE.Vector3()const tangent = new THREE.Vector3()let travelled = 0
function update(dt: number, speed = 3) {  travelled = (travelled + speed * dt) % length  const u = travelled / length    // fraction of the length, not t  path.getPointAt(u, pos)         // getPoint(t) would be the naive version  path.getTangentAt(u, tangent)  ship.position.copy(pos)  ship.lookAt(pos.add(tangent))}
// After moving control points, rebuild the table:path.needsUpdate = true

Common mistakes

  • Driving motion with raw t and getting a speed-up through the middle of the curve.
  • Changing control points and forgetting curve.needsUpdate = true, so getPointAt uses a stale table.
  • Building the arc-length table every frame for a static curve.
  • Joining curves with handles that are not collinear, leaving a visible kink.
  • Normalizing a tangent where it is zero (a handle placed exactly on its endpoint). Fall back to the next control point.

Prompt

Prompt for your AI assistant
Write `src/lib/bezier.ts` in TypeScript: `cubicPoint(p0, p1, p2, p3, t, out)` and `cubicTangent(...)` writing into a THREE.Vector3 without allocating; `buildArcTable(curve, samples = 128)` returning a Float32Array of cumulative chord lengths at evenly spaced t; and `tAtDistance(table, s)` that binary-searches the table and linearly interpolates, clamping s to [0, length]. Then add a `PathFollower` for Three.js that moves an Object3D along the curve at a constant `speed` (units/second) using the table, faces along the tangent with lookAt(position + tangent), and loops or ping-pongs. Write Vitest tests: de Casteljau equals the Bernstein form for 11 values of t; the table is strictly increasing; 20 equal-distance samples have chord lengths within 2% of each other while equal-t samples do not.

In learning paths

Further reading

Updated 2026-11-01