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 to each parameter value between and . As increases, that point slides along the curve. Its velocity is the derivative , a vector that points in the direction of motion, and the speed is the length of that vector:
The speed measures distance traveled per unit of . Speed times an infinitesimal step gives the distance covered during that step, so adding up those small pieces gives the total:
Here is the curve parameter, is a dummy variable of integration, is the arc length accumulated from the start up to parameter , and 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 .
The important practical consequence is that is not distance. Equal steps in produce unequal steps in distance whenever the speed changes along the curve. A renderer that samples evenly in 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 against 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
Differentiating gives the velocity:
That expression is linear in , which is the key to the closed form. Collect the constant and linear parts by defining two new vectors:
Now the velocity is simply
You can check this by expanding : it equals , the derivative above. Because the derivative is a straight line in , its squared length is a quadratic polynomial:
with the three scalar coefficients
The speed is the square root of that quadratic, and the length is its integral:
To evaluate it, complete the square inside the root. Write and . Then
Substituting turns the integral into a standard form:
The antiderivative of for is
so the exact quadratic arc length is
The quantity is never negative for a quadratic Bezier curve. The product can never exceed by the Cauchy-Schwarz inequality, and that is exactly the condition that makes . When and are parallel, is zero, the logarithm becomes singular, and the root simplifies to . The integral of the absolute value is easy to evaluate piecewise.
Worked example
Take , , and . Then
The coefficients are , , and . The shift is
and the offset is
Evaluating at the two endpoints gives and . With , the length is
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 is the midpoint of and , 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:
Its derivative is
Each term is a vector times a quadratic polynomial in , so is a vector of degree two, and is a single quartic polynomial in . 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:
The points are the roots of the -th Legendre polynomial, and the weights are chosen so that the rule is exact for every polynomial up to degree . Our interval is rather than , so substitute , which gives
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 rather than the whole curve. Keep the abscissae and weights, and scale the interval to :
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 |
|---|---|---|---|---|
| 6.214707537 | ||||
| 3.443380724 | ||||
| 1.154700538 |
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 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
Because speed is never negative, increases steadily from to as goes from to , so it can be inverted. The inverse, written , 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 at many evenly spaced 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 using the derivative , 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 , advance the distance by 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 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 over the parameter range. For a quadratic curve, the velocity is linear in , so the speed is the square root of a quadratic polynomial and the integral has the closed form 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.