Bézier curves and splines
Bézier curves bend smoothly towards control points without passing through them. Evaluate them with de Casteljau’s algorithm, find their tangents and lengths, and chain them into smooth splines.
De Casteljau: interpolate, and repeat
var pts = new V2[] { new(0, 0), new(1, 3), new(4, 3), new(5, 0) }; foreach (double t in new[] { 0, 0.25, 0.5, 0.75, 1 }) Console.WriteLine($"t = {t,-4} {Bezier(pts, t)}"); static V2 Bezier(V2[] control, double t){ var p = (V2[])control.Clone(); for (int n = p.Length - 1; n > 0; n--) // each pass leaves one point fewer for (int i = 0; i < n; i++) p[i] = p[i] + (p[i + 1] - p[i]) * t; return p[0];} readonly record struct V2(double X, double Y){ public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y); public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y); public static V2 operator *(V2 v, double k) => new(v.X * k, v.Y * k); public static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y; public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X; public double Length => Math.Sqrt(X * X + Y * Y); public override string ToString() => $"({X:0.###}, {Y:0.###})";}t = 0 (0, 0) t = 0.25 (1.063, 1.688) t = 0.5 (2.5, 2.25) t = 0.75 (3.938, 1.688) t = 1 (5, 0)
The formula behind it
Tangent and length
var pts = new V2[] { new(0, 0), new(1, 3), new(4, 3), new(5, 0) }; for (int n = 4; n <= 256; n *= 4){ double length = 0; V2 prev = pts[0]; for (int i = 1; i <= n; i++) { V2 p = Bezier(pts, (double)i / n); length += (p - prev).Length; prev = p; } Console.WriteLine($"{n,3} segments: {length:F4}");} static V2 Bezier(V2[] control, double t){ var p = (V2[])control.Clone(); for (int n = p.Length - 1; n > 0; n--) for (int i = 0; i < n; i++) p[i] = p[i] + (p[i + 1] - p[i]) * t; return p[0];} readonly record struct V2(double X, double Y){ public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y); public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y); public static V2 operator *(V2 v, double k) => new(v.X * k, v.Y * k); public static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y; public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X; public double Length => Math.Sqrt(X * X + Y * Y); public override string ToString() => $"({X:0.###}, {Y:0.###})";}4 segments: 7.0755 16 segments: 7.1835 64 segments: 7.1902 256 segments: 7.1906
Splines: chaining curves
| Spline | Passes through its points? | Typical use |
|---|---|---|
var p = new V2[] { new(0, 0), new(2, 2), new(5, 2), new(7, 0) }; // surveyed points // The span from p[1] to p[2], as a cubic BezierV2 c1 = p[1] + (p[2] - p[0]) * (1.0 / 6);V2 c2 = p[2] - (p[3] - p[1]) * (1.0 / 6);var span = new[] { p[1], c1, c2, p[2] }; Console.WriteLine($"Control points {c1} and {c2}");Console.WriteLine($"Halfway: {Bezier(span, 0.5)}"); static V2 Bezier(V2[] control, double t){ var q = (V2[])control.Clone(); for (int n = q.Length - 1; n > 0; n--) for (int i = 0; i < n; i++) q[i] = q[i] + (q[i + 1] - q[i]) * t; return q[0];} readonly record struct V2(double X, double Y){ public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y); public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y); public static V2 operator *(V2 v, double k) => new(v.X * k, v.Y * k); public static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y; public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X; public double Length => Math.Sqrt(X * X + Y * Y); public override string ToString() => $"({X:0.###}, {Y:0.###})";}Control points (2.833, 2.333) and (4.167, 2.333) Halfway: (3.5, 2.25)
Tangent(control, t) for a cubic Bézier using the derivative formula. For the control points (0, 0), (0, 2), (3, 3) and (4, 3), print the curve point and the unit tangent at t = 0, 0.5 and 1.
Show a hint
Show the solution
var pts = new V2[] { new(0, 0), new(0, 2), new(3, 3), new(4, 3) }; foreach (double t in new[] { 0, 0.5, 1 }){ V2 d = Tangent(pts, t); Console.WriteLine($"t = {t}: point {Bezier(pts, t)}, tangent {d * (1 / d.Length)}");} static V2 Tangent(V2[] p, double t){ double s = 1 - t; return ((p[1] - p[0]) * (s * s) + (p[2] - p[1]) * (2 * s * t) + (p[3] - p[2]) * (t * t)) * 3;} static V2 Bezier(V2[] control, double t){ var p = (V2[])control.Clone(); for (int n = p.Length - 1; n > 0; n--) for (int i = 0; i < n; i++) p[i] = p[i] + (p[i + 1] - p[i]) * t; return p[0];} readonly record struct V2(double X, double Y){ public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y); public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y); public static V2 operator *(V2 v, double k) => new(v.X * k, v.Y * k); public static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y; public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X; public double Length => Math.Sqrt(X * X + Y * Y); public override string ToString() => $"({X:0.###}, {Y:0.###})";}t = 0: point (0, 0), tangent (0, 1) t = 0.5: point (1.625, 2.25), tangent (0.868, 0.496) t = 1: point (4, 3), tangent (1, 0)
Remember
A cubic Bézier starts at P0 towards P1 and ends at P3 from P2; it passes through neither P1 nor P2. De Casteljau’s algorithm evaluates it by repeated interpolation: 4 points, then 3, 2 and 1. The Bernstein weights add up to 1, so the curve stays inside its control polygon. The tangent is B′(t); the length has no formula, so sum chords and increase the count until it settles. Splines chain curves; line up the control points at each joint to avoid kinks. Catmull-Rom passes through every point.