Last updated on

Bezier Curve Arc Length Formula

ENNLESPT-BR


The arc length of a Bezier curve is the total distance a point travels as it moves from the start of the curve to the end. You get it by integrating the speed of that point, which is the length of the curve derivative, across the parameter range. A quadratic Bezier curve has an exact closed form. A cubic Bezier curve generally does not, so implementations approximate its length with numerical quadrature. This page derives the quadratic formula, builds the cubic approximation, and shows how to turn arc length into a length parametrization that moves a point at constant speed. The interactive Bezier curves tutorial covers the control point geometry that everything here builds on.

Arc Length Is the Speed Integral

A Bezier curve assigns a point B(t)B(t) to each parameter value tt between 00 and 11. As tt increases, that point slides along the curve. Its velocity is the derivative B′(t)B'(t), a vector that points in the direction of motion, and the speed is the length of that vector:

∣B′(t)∣=Bx′(t)2+By′(t)2|B'(t)| = \sqrt{B_x'(t)^2 + B_y'(t)^2}

The speed measures distance traveled per unit of tt. Speed times an infinitesimal step dtdt gives the distance covered during that step, so adding up those small pieces gives the total:

s(t)=∫0t∣B′(τ)∣ dτ,L=s(1)=∫01∣B′(t)∣ dts(t) = \int_0^t |B'(\tau)|,d\tau, \qquad L = s(1) = \int_0^1 |B'(t)|,dt

Here tt is the curve parameter, τ\tau is a dummy variable of integration, s(t)s(t) is the arc length accumulated from the start up to parameter tt, and LL is the full length of the curve. The straight line between the endpoints is always shorter than the curve, and the control polygon, which connects the control points with straight segments, is always longer. The true length sits between those two bounds, and it equals the area under the speed curve plotted against tt.

The important practical consequence is that tt is not distance. Equal steps in tt produce unequal steps in distance whenever the speed changes along the curve. A renderer that samples evenly in tt will place more points where the curve is slow and fewer where it is fast.

The left panel is an editable quadratic or cubic curve. The right panel plots the corresponding speed ∣B′(t)∣|B'(t)| against tt and shades the area under it. That shaded area is the arc length printed in the corner. Dragging a handle changes both panels at once: spreading the handles apart raises the speed curve and grows the area, while a compact shape lowers the curve and shrinks it. Switch to the cubic case and the speed curve can develop several humps, because the cubic derivative is a quadratic polynomial whose length changes more freely than the linear quadratic derivative.

Finding the Full Length of a Quadratic Bezier Curve

A quadratic Bezier curve uses three points and the formula

B(t)=(1−t)2P0+2(1−t)tP1+t2P2B(t) = (1 - t)^2 P_0 + 2(1 - t)t P_1 + t^2 P_2

Differentiating gives the velocity:

B′(t)=2(1−t)(P1−P0)+2t(P2−P1)B'(t) = 2(1 - t)(P_1 - P_0) + 2t(P_2 - P_1)

That expression is linear in tt, which is the key to the closed form. Collect the constant and linear parts by defining two new vectors:

A=P0−2P1+P2,B=2(P1−P0)A = P_0 - 2P_1 + P_2, \qquad B = 2(P_1 - P_0)

Now the velocity is simply

B′(t)=2At+BB'(t) = 2A t + B

You can check this by expanding 2At+B2At + B: it equals 2(1−t)(P1−P0)+2t(P2−P1)2(1-t)(P_1 - P_0) + 2t(P_2 - P_1), the derivative above. Because the derivative is a straight line in tt, its squared length is a quadratic polynomial:

∣B′(t)∣2=(2At+B)⋅(2At+B)=at2+bt+c|B'(t)|^2 = (2At + B)\cdot(2At + B) = a t^2 + b t + c

with the three scalar coefficients

a=4(A⋅A),b=4(A⋅B),c=B⋅Ba = 4(A \cdot A), \qquad b = 4(A \cdot B), \qquad c = B \cdot B

The speed is the square root of that quadratic, and the length is its integral:

L=∫01at2+bt+c dtL = \int_0^1 \sqrt{a t^2 + b t + c},dt

To evaluate it, complete the square inside the root. Write k=b/(2a)k = b / (2a) and m=c/a−k2m = c/a - k^2. Then

at2+bt+c=a((t+k)2+m)a t^2 + b t + c = a\left((t + k)^2 + m\right)

Substituting u=t+ku = t + k turns the integral into a standard form:

L=a∫kk+1u2+m duL = \sqrt{a}\int_k^{k+1} \sqrt{u^2 + m},du

The antiderivative of u2+m\sqrt{u^2 + m} for m>0m > 0 is

F(u)=12(uu2+m+mln⁡(u+u2+m))F(u) = \frac{1}{2}\left(u\sqrt{u^2 + m} + m\ln\left(u + \sqrt{u^2 + m}\right)\right)

so the exact quadratic arc length is

L=a (F(k+1)−F(k))L = \sqrt{a},\bigl(F(k+1) - F(k)\bigr)

The quantity mm is never negative for a quadratic Bezier curve. The product (A⋅B)2(A \cdot B)^2 can never exceed (A⋅A)(B⋅B)(A \cdot A)(B \cdot B) by the Cauchy-Schwarz inequality, and that is exactly the condition that makes m≥0m \ge 0. When AA and BB are parallel, mm is zero, the logarithm becomes singular, and the root simplifies to a ∣t+k∣\sqrt{a},|t + k|. The integral of the absolute value is easy to evaluate piecewise.

Worked example

Take P0=(0,0)P_0 = (0,0), P1=(1,2)P_1 = (1,2), and P2=(3,0)P_2 = (3,0). Then

A=(0,0)−2(1,2)+(3,0)=(1,−4),B=2(1,2)−2(0,0)=(2,4)A = (0,0) - 2(1,2) + (3,0) = (1,-4), \qquad B = 2(1,2) - 2(0,0) = (2,4)

The coefficients are a=4(1+16)=68a = 4(1 + 16) = 68, b=4(2−16)=−56b = 4(2 - 16) = -56, and c=4+16=20c = 4 + 16 = 20. The shift is

k=−562⋅68=−717≈−0.4118k = \frac{-56}{2 \cdot 68} = -\frac{7}{17} \approx -0.4118

and the offset is

m=2068−(−717)2=36289≈0.1246m = \frac{20}{68} - \left(-\frac{7}{17}\right)^2 = \frac{36}{289} \approx 0.1246

Evaluating FF at the two endpoints gives F(1+k)≈0.2169F(1 + k) \approx 0.2169 and F(k)≈−0.2385F(k) \approx -0.2385. With a≈8.2462\sqrt{a} \approx 8.2462, the length is

L≈8.2462×(0.2169+0.2385)≈3.7546L \approx 8.2462 \times (0.2169 + 0.2385) \approx 3.7546

Quadratic arc length in code

This function returns the exact length, including the straight-line and parallel-velocity cases:

function quadraticArcLength(P0, P1, P2) {
  // A = P0 - 2 P1 + P2 and B = 2 (P1 - P0) make B'(t) = 2 A t + B.
  const ax = P0.x - 2 * P1.x + P2.x;
  const ay = P0.y - 2 * P1.y + P2.y;
  const bx = 2 * (P1.x - P0.x);
  const by = 2 * (P1.y - P0.y);

  const a = 4 * (ax * ax + ay * ay);
  const b = 4 * (ax * bx + ay * by);
  const c = bx * bx + by * by;

  // Constant speed: the curve is a straight line.
  if (a === 0) return Math.sqrt(c);

  const k = b / (2 * a);
  const scale = Math.sqrt(a);
  const disc = b * b - 4 * a * c;

  // A parallel to B: the speed is sqrt(a) * |t + k|.
  if (Math.abs(disc) < 1e-12 * a * c + 1e-30) {
    const abs = (x) => 0.5 * x * Math.abs(x);
    return scale * (abs(1 + k) - abs(k));
  }

  const m = c / a - k * k;
  const F = (u) => 0.5 * (u * Math.sqrt(u * u + m) + m * Math.log(u + Math.sqrt(u * u + m)));
  return scale * (F(1 + k) - F(k));
}

The a === 0 branch triggers when P1P_1 is the midpoint of P0P_0 and P2P_2, which collapses the curve to a straight line. The near-zero discriminant branch catches the situation where the velocity stays on one line but may reverse direction, so the curve travels out and back. For the common case the closed form runs in constant time with no sampling.

Why the Arc Length of a Cubic Bezier Has No Closed Form

A cubic Bezier curve uses four points:

B(t)=(1−t)3P0+3(1−t)2tP1+3(1−t)t2P2+t3P3B(t) = (1 - t)^3 P_0 + 3(1 - t)^2t P_1 + 3(1 - t)t^2 P_2 + t^3 P_3

Its derivative is

B′(t)=3(1−t)2(P1−P0)+6(1−t)t(P2−P1)+3t2(P3−P2)B'(t) = 3(1 - t)^2 (P_1 - P_0) + 6(1 - t)t (P_2 - P_1) + 3t^2 (P_3 - P_2)

Each term is a vector times a quadratic polynomial in tt, so B′(t)B'(t) is a vector of degree two, and ∣B′(t)∣2|B'(t)|^2 is a single quartic polynomial in tt. The speed is the square root of that quartic. Integrals of the square root of a general quartic are elliptic integrals, and elliptic integrals have no expression in elementary functions. There is no logarithmic or square-root formula that covers every cubic the way the completed-square formula covers every quadratic.

That does not mean the length is unknowable. It means the length has to be computed rather than looked up, and the standard tool for that job is Gaussian quadrature.

Approximating the Arc Length of a Cubic Bezier

Gauss-Legendre quadrature approximates a definite integral by sampling the integrand at a small set of points and taking a weighted sum:

∫−11f(x) dx≈∑i=1nwif(xi)\int_{-1}^{1} f(x),dx \approx \sum_{i=1}^{n} w_i f(x_i)

The points xix_i are the roots of the nn-th Legendre polynomial, and the weights wiw_i are chosen so that the rule is exact for every polynomial up to degree 2n−12n - 1. Our interval is [0,1][0,1] rather than [−1,1][-1,1], so substitute t=(x+1)/2t = (x + 1)/2, which gives

∫01f(t) dt=12∑i=1nwif ⁣(xi+12)\int_0^1 f(t),dt = \frac{1}{2}\sum_{i=1}^{n} w_i f!\left(\frac{x_i + 1}{2}\right)

Sixteen points make the rule exact for polynomials up to degree 31. Speed is a square root rather than a polynomial, so the rule is not exact, but for a smooth speed function 16 points already land within a few parts in a million of the true length.

// 16-point Gauss-Legendre abscissae and weights on [-1, 1].
const GL_X = [
  -0.989400934991650, -0.944575023073233, -0.865631202387832, -0.755404408355003,
  -0.617876244402644, -0.458016777657227, -0.281603550779259, -0.095012509837637,
   0.095012509837637,  0.281603550779259,  0.458016777657227,  0.617876244402644,
   0.755404408355003,  0.865631202387832,  0.944575023073233,  0.989400934991650,
];
const GL_W = [
  0.027152459411754, 0.062253523938648, 0.095158511682493, 0.124628971255534,
  0.149595988816577, 0.169156519395003, 0.182603415044924, 0.189450610455069,
  0.189450610455069, 0.182603415044924, 0.169156519395003, 0.149595988816577,
  0.124628971255534, 0.095158511682493, 0.062253523938648, 0.027152459411754,
];

function cubicSpeed(t, P) {
  const u = 1 - t;
  const dx = 3 * u * u * (P[1].x - P[0].x) + 6 * u * t * (P[2].x - P[1].x) + 3 * t * t * (P[3].x - P[2].x);
  const dy = 3 * u * u * (P[1].y - P[0].y) + 6 * u * t * (P[2].y - P[1].y) + 3 * t * t * (P[3].y - P[2].y);
  return Math.sqrt(dx * dx + dy * dy);
}

function cubicArcLength(P) {
  let sum = 0;
  for (let i = 0; i < GL_X.length; i++) {
    const t = 0.5 * GL_X[i] + 0.5;
    sum += GL_W[i] * cubicSpeed(t, P);
  }
  return 0.5 * sum;
}

The same helper works when the quadrature needs the length from the start to an arbitrary parameter tt rather than the whole curve. Keep the abscissae and weights, and scale the interval to [0,t][0, t]:

function cubicArcLengthTo(t, P) {
  if (t <= 0) return 0;
  let sum = 0;
  for (let i = 0; i < GL_X.length; i++) {
    const s = 0.5 * t * (GL_X[i] + 1);
    sum += GL_W[i] * cubicSpeed(s, P);
  }
  return 0.5 * t * sum;
}

How accurate is the quadrature?

The table compares the rule against a high-precision reference for three cubic curves. The errors are absolute.

Cubic control points True length 8 points 16 points 24 points
(0,0) (1,3) (4,−2) (5,1)(0,0)\ (1,3)\ (4,-2)\ (5,1) 6.214707537 1.9×10−41.9\times10^{-4} 7.1×10−77.1\times10^{-7} 2.2×10−102.2\times10^{-10}
(0,0) (1,1) (2,1) (3,0)(0,0)\ (1,1)\ (2,1)\ (3,0) 3.443380724 6.2×10−86.2\times10^{-8} 1.6×10−141.6\times10^{-14} <10−15<10^{-15}
(0,0) (0,1) (0,−1) (0,0)(0,0)\ (0,1)\ (0,-1)\ (0,0) 1.154700538 1.5×10−21.5\times10^{-2} 3.8×10−33.8\times10^{-3} 2.4×10−32.4\times10^{-3}

The first two curves have smooth speed profiles, and 16 points are plenty. The third curve doubles back on itself, so its speed drops to zero and the speed function has a corner. Quadrature converges slowly at a corner, and adding more points barely helps. The fix is to split the parameter range at the troublesome spot and integrate each piece separately, or to use the subdivision method below.

Adaptive subdivision as an alternative

De Casteljau’s algorithm can split a cubic segment into two smaller cubic segments at any parameter value, and splitting at 0.50.5 is especially simple: average neighboring control points until one point remains. The control polygon of a small piece is nearly a straight line, so its length and the endpoint chord length are both close to the true arc length. The gap between them measures how curved the piece still is. When the gap is below a tolerance, return the average of the two; otherwise split and recurse.

const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
const midpoint = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });

function adaptiveArcLength(P0, P1, P2, P3, tolerance = 1e-6) {
  const chord = distance(P0, P3);
  const polygon =
    distance(P0, P1) + distance(P1, P2) + distance(P2, P3);
  if (polygon - chord <= tolerance) {
    return (polygon + chord) / 2;
  }

  // Split at t = 0.5 with de Casteljau averaging.
  const p01 = midpoint(P0, P1);
  const p12 = midpoint(P1, P2);
  const p23 = midpoint(P2, P3);
  const p012 = midpoint(p01, p12);
  const p123 = midpoint(p12, p23);
  const mid = midpoint(p012, p123);

  return (
    adaptiveArcLength(P0, p01, p012, mid, tolerance) +
    adaptiveArcLength(mid, p123, p23, P3, tolerance)
  );
}

Adaptive subdivision costs more evaluations than a single 16-point rule, but it concentrates work where the curve bends and it keeps working on curves with corners or near-cusps. The quadrature rule is faster for the smooth curves that appear in most rendering and animation work, and subdivision is the safe fallback when a tolerance has to be guaranteed.

Bezier Curve Length Parametrization

Everything so far computes length from the parameter. The reverse question, finding the parameter that corresponds to a given distance, is what makes constant-speed motion possible. Define the accumulated length function

s(t)=∫0t∣B′(τ)∣ dτs(t) = \int_0^t |B'(\tau)|,d\tau

Because speed is never negative, s(t)s(t) increases steadily from 00 to LL as tt goes from 00 to 11, so it can be inverted. The inverse, written t(s)t(s), is the length parametrization: feed it a distance and it returns the parameter that puts the point exactly that far along the curve. The forward direction has a closed form for quadratic curves and a quadrature rule for cubic curves, but neither inverse has an elementary expression in general, so implementations build a table and interpolate.

The recipe has three steps. Sample s(t)s(t) at many evenly spaced tt values and store the pairs. To convert a target distance to a parameter, binary search the table for the surrounding pair, then linearly interpolate. Because the table is only a piecewise-linear approximation of a smooth function, finish with one or two Newton steps. Newton’s method finds a root of s(t)−stargets(t) - s_{\text{target}} using the derivative s′(t)=∣B′(t)∣s'(t) = |B'(t)|, which is exactly the speed that is already available.

Both panels show the same cubic curve. The left panel places a dot every tenth of the parameter range, and the right panel places a dot every tenth of the total arc length. Dragging a control point reshapes both panels together. Where the curve moves slowly, the parameter dots crowd together, and where it moves quickly, they spread apart. The arc length dots stay evenly spaced because the table maps each equal distance back to the parameter that reaches it. The red markers travel both curves in the same time. The left marker speeds up and slows down with the parameter, and the right marker moves at a steady pace.

Building and inverting the length table

function buildArcLengthTable(P, samples = 128) {
  const ts = new Float64Array(samples + 1);
  const ss = new Float64Array(samples + 1);
  for (let i = 1; i <= samples; i++) {
    ts[i] = i / samples;
    ss[i] = cubicArcLengthTo(ts[i], P);
  }
  return { ts, ss, length: ss[samples] };
}

function parameterAtLength(table, target, P) {
  const { ts, ss } = table;

  // Binary search for the pair of samples that bracket the target.
  let lo = 0;
  let hi = ts.length - 1;
  while (lo + 1 < hi) {
    const mid = (lo + hi) >> 1;
    if (ss[mid] <= target) lo = mid;
    else hi = mid;
  }

  // Linear interpolation gives a good starting point.
  const span = ss[hi] - ss[lo];
  let t = span > 0 ? ts[lo] + ((target - ss[lo]) / span) * (ts[hi] - ts[lo]) : ts[lo];

  // Newton steps refine the answer to full precision.
  for (let i = 0; i < 3; i++) {
    const speed = cubicSpeed(t, P);
    if (speed < 1e-9) break;
    t -= (cubicArcLengthTo(t, P) - target) / speed;
    t = Math.min(1, Math.max(0, t));
  }

  return t;
}

The table is built once per curve shape. Changing a control point invalidates it, but moving a point along the curve at runtime only performs lookups. To move an object at constant speed vv, advance the distance by vv times the elapsed time and call parameterAtLength with the result:

const table = buildArcLengthTable(P);
let travelled = 0;
let previous = performance.now();

function step(now, targetSpeed) {
  travelled += targetSpeed * (now - previous) / 1000;
  previous = now;
  const distance = travelled % table.length;
  const t = parameterAtLength(table, distance, P);
  // Sample the curve at t to place the object.
}

Without the table lookups, advancing the parameter directly would make the object lurch through the fast parts of the curve and crawl through the slow parts.

Checking the Implementation

A few assertions cover the main branches. The quadratic closed form should match the quadrature rule on the same curve, because the quadrature rule is accurate enough at this scale. The table round trip should return the original distance after converting to a parameter and back.

const q = [{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 3, y: 0 }];
console.assert(Math.abs(quadraticArcLength(q[0], q[1], q[2]) - 3.7546364123) < 1e-9);

// A straight-line quadratic has length equal to its endpoint distance.
console.assert(Math.abs(quadraticArcLength({ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }) - Math.SQRT2 * 2) < 1e-9);

const c = [{ x: 0, y: 0 }, { x: 1, y: 3 }, { x: 4, y: -2 }, { x: 5, y: 1 }];
console.assert(Math.abs(cubicArcLength(c) - 6.2147075374) < 1e-5);

const table = buildArcLengthTable(c);
for (let i = 0; i <= 10; i++) {
  const target = (i / 10) * table.length;
  const t = parameterAtLength(table, target, c);
  console.assert(Math.abs(cubicArcLengthTo(t, c) - target) < 1e-6);
}

Edge Cases and Practical Notes

Straight lines. When a quadratic control point sits exactly at the midpoint of its neighbors, the curve collapses to a straight segment, and the endpoint chord is the full length. The quadratic branch handles this case directly. A cubic whose control points are collinear and whose direction never reverses also traces a straight segment, and the quadrature returns the correct value because the speed is constant along the line.

Stationary points and cusps. A cubic curve can have a parameter value where its velocity is zero, which happens when the curve reverses direction or forms a cusp. The speed function touches zero there and has a sharp corner, and Gaussian quadrature loses accuracy. The adaptive subdivision method above is the reliable choice for these shapes.

Self-intersections. A loop-shaped cubic crosses itself, but its arc length keeps counting after the crossing. That matches the distance a point travels, which is usually what animation and offsetting need. If you instead want the length of the visible outline, split the curve at the crossing and count each branch once.

Two dimensions and three dimensions. The formulas only use vector addition, dot products, and lengths, so the same code works for 2D and 3D control points. A 3D control point simply has a zz field, and the speed picks up a third squared term.

Precompute when the shape is fixed. A length table for a static path can be built once and stored. A tool that only reads the total length can skip the table entirely and call the quadrature, which is a handful of multiplications and square roots.

Curve reversal and symmetry. Reversing the control point order leaves the length unchanged, and so does the closed form and the quadrature rule. This is a useful sanity check when a curve is built from either end.

Summary

The arc length of a Bezier curve is the integral of the speed ∣B′(t)∣|B'(t)| over the parameter range. For a quadratic curve, the velocity is linear in tt, so the speed is the square root of a quadratic polynomial and the integral has the closed form L=a(F(k+1)−F(k))L = \sqrt{a}(F(k+1) - F(k)) derived above. For a cubic curve, the speed is the square root of a quartic polynomial, which has no elementary antiderivative, so a 16-point Gauss-Legendre rule computes the length to about six significant digits, and adaptive subdivision covers curves with corners. Inverting the accumulated length gives the length parametrization, which a lookup table, binary search, and a few Newton steps implement for constant-speed motion. The interactive Bezier curves tutorial covers the control point geometry behind these lengths.