t is not distance along a curve
A Bezier is defined by a parameter t from 0 to 1, and it is enormously
tempting to treat that as position along the curve. It is not. The curve moves at
different speeds through different parts of its own parameter range, and everything
you place by t inherits that.
Left, the markers bunch up in the tight hook and stretch out along the lazy opening,
even though t was stepped in perfectly equal increments. Right, the table places them at approximately equal distances along the curve.
Why the parameter is not the distance
The speed along a curve is the magnitude of its derivative, |B'(t)|.
For control points A, B, C and D, the cubic's derivative is:
B'(t) = 3[(1-t)^2(B-A) + 2(1-t)t(C-B) + t^2(D-C)]
Those differences are vectors. Their directions matter as well as their lengths: opposing vectors can cancel and bring the speed to zero. The ratio of control-polygon segment lengths doesn't reliably tell you the speed ratio. Evaluate the derivative or measure the spacing error instead.
Even a straight cubic can have uneven parameter speed if its control points aren't evenly spaced. For this curve, the maximum speed is about 3.08 times the minimum.
The fix is a small table
// build once
float len[N+1]; len[0] = 0;
for (i = 1..N) len[i] = len[i-1] + |B(i/N) - B((i-1)/N)|;
// then invert: given a target LENGTH, find the t
find i where len[i] >= target
f = (target - len[i-1]) / (len[i] - len[i-1])
t = (i-1 + f) / N
A general cubic has no elementary arc-length formula. Numerical integration and cumulative chord-length tables are common approaches. This demo uses 20 chord segments and linear interpolation within each table interval. For this specific curve, the resulting marker intervals differ from equal arc length by less than 1%. That's a measured approximation, not a guarantee for other control points.
Increase the sample count and compare the result, or subdivide adaptively to an error tolerance. Tight bends and cusps need particular care. A curve with zero total length needs a separate case: every marker belongs at the same point.
Everything that lands on a curve needs this
- Dashes and dots. Uneven spacing is the most visible version.
- Anything repeated along a stroke: studs, scales, rungs, a chain.
- Constant-speed motion. A thing traveling by
tspeeds up and slows down for no reason, which reads as a physics bug. - Texture along a stroke. A pattern parameterised by
tstretches and squashes. - Flattening to line segments. Choose subdivisions from curvature and an error tolerance. Equal arc-length spacing alone does not guarantee an accurate polyline.
Rules of thumb
tis a parameter, not a distance. Equal parameter steps give equal distances only when parameter speed is constant.- Check the derivative or the resulting spacing before deciding the error is acceptable.
- Build a cumulative length table and invert it. Choose the sample count from the error you can accept.
- Interpolate inside the table bracket rather than snapping to the nearest sample.
- In an application, cache the table until the curve changes. This self-contained fragment shader recomputes it per fragment for simplicity.
References: derivatives of a Bezier curve and numerical arc-length methods. The segment distance function follows Inigo Quilez; the output dither uses Jorge Jimenez's interleaved gradient noise.
If you find anything wrong, let me know at hello@andrewdetwiler.com and I'll get it updated.
One email when something new goes up. No newsletter, no schedule, nothing else.
Double opt-in, so watch for a confirmation email. Unsubscribe any time.