Bézier Curves: How to Draw Beautiful Smooth Curves With Code
The Bézier curve is one of the most important mathematical tools in computer graphics. Every font you read, every vector logo you see, every smooth animation path — they all use Bézier curves. Invented independently by Pierre Bézier (Renault) and Paul de Casteljau (Citroën) in the 1960s for car body design, these curves are now the backbone of digital design, from Photoshop pen tools to CSS animations to SVG paths.
In this guide, we'll build 8 interactive Bézier curve examples from scratch in JavaScript and Canvas. You'll understand not just how to use them, but why they work — the elegant mathematics of interpolation that makes smooth curves from just a few control points.
What is a Bézier curve?
A Bézier curve is defined by a set of control points. The curve starts at the first point and ends at the last point, but doesn't necessarily pass through the middle points — instead, the middle points pull the curve toward them like magnets. The mathematical foundation is surprisingly simple: it's just repeated linear interpolation (lerp).
The lerp function — the building block of all Bézier curves:
// Linear interpolation: blend between a and b by factor t (0 to 1)
function lerp(a, b, t) {
return a + (b - a) * t;
}
That's it. Every Bézier curve, no matter how complex, is built from this single operation applied recursively. When t = 0, you get point a. When t = 1, you get point b. Values between 0 and 1 give you smooth points in between.
1. Linear Bézier (straight line interpolation)
The simplest Bézier "curve" is just a straight line between two points. While not curved, it demonstrates the core concept: sweeping a parameter t from 0 to 1 traces the path. This is the foundation that all higher-order curves build upon.
var canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var t = 0, dir = 1;
var P0 = { x: 80, y: 300 };
var P1 = { x: 520, y: 100 };
function lerp(a, b, t) { return a + (b - a) * t; }
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 400);
// Draw control line
ctx.strokeStyle = 'rgba(100, 200, 255, 0.3)';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(P0.x, P0.y);
ctx.lineTo(P1.x, P1.y);
ctx.stroke();
ctx.setLineDash([]);
// Draw traced path up to current t
ctx.strokeStyle = '#ff6b9d';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(P0.x, P0.y);
for (var i = 0; i <= t * 100; i++) {
var s = i / 100;
ctx.lineTo(lerp(P0.x, P1.x, s), lerp(P0.y, P1.y, s));
}
ctx.stroke();
// Moving point
var px = lerp(P0.x, P1.x, t);
var py = lerp(P0.y, P1.y, t);
ctx.beginPath();
ctx.arc(px, py, 8, 0, Math.PI * 2);
ctx.fillStyle = '#ff6b9d';
ctx.fill();
// Control points
[P0, P1].forEach(function(p) {
ctx.beginPath();
ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);
ctx.fillStyle = '#64c8ff';
ctx.fill();
});
// Label
ctx.fillStyle = '#fff';
ctx.font = '14px monospace';
ctx.fillText('t = ' + t.toFixed(2), 20, 30);
t += dir * 0.008;
if (t >= 1) { t = 1; dir = -1; }
if (t <= 0) { t = 0; dir = 1; }
requestAnimationFrame(draw);
}
draw();
2. Quadratic Bézier curve
A quadratic Bézier uses three control points: start (P0), control (P1), and end (P2). The curve is computed by lerping between two linear segments simultaneously — lerp between P0→P1 and P1→P2 at the same t value, then lerp between those two intermediate points. This "lerp of lerps" creates the characteristic smooth parabolic shape you see in fonts and UI design.
var canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var t = 0, dir = 1;
var P0 = { x: 60, y: 340 };
var P1 = { x: 300, y: 40 };
var P2 = { x: 540, y: 340 };
function lerp(a, b, t) { return a + (b - a) * t; }
function quadBezier(p0, p1, p2, t) {
var x01 = lerp(p0.x, p1.x, t), y01 = lerp(p0.y, p1.y, t);
var x12 = lerp(p1.x, p2.x, t), y12 = lerp(p1.y, p2.y, t);
return { x: lerp(x01, x12, t), y: lerp(y01, y12, t) };
}
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 400);
// Control polygon
ctx.strokeStyle = 'rgba(100, 200, 255, 0.3)';
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(P0.x, P0.y);
ctx.lineTo(P1.x, P1.y);
ctx.lineTo(P2.x, P2.y);
ctx.stroke();
ctx.setLineDash([]);
// Intermediate lerp lines
var x01 = lerp(P0.x, P1.x, t), y01 = lerp(P0.y, P1.y, t);
var x12 = lerp(P1.x, P2.x, t), y12 = lerp(P1.y, P2.y, t);
ctx.strokeStyle = 'rgba(255, 200, 50, 0.5)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x01, y01);
ctx.lineTo(x12, y12);
ctx.stroke();
// Full curve
ctx.strokeStyle = '#ff6b9d';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(P0.x, P0.y);
for (var i = 1; i <= 100; i++) {
var pt = quadBezier(P0, P1, P2, i / 100);
ctx.lineTo(pt.x, pt.y);
}
ctx.stroke();
// Moving point on curve
var cur = quadBezier(P0, P1, P2, t);
ctx.beginPath();
ctx.arc(cur.x, cur.y, 8, 0, Math.PI * 2);
ctx.fillStyle = '#ff6b9d';
ctx.fill();
// Intermediate points
ctx.beginPath(); ctx.arc(x01, y01, 5, 0, Math.PI * 2);
ctx.fillStyle = '#ffc832'; ctx.fill();
ctx.beginPath(); ctx.arc(x12, y12, 5, 0, Math.PI * 2);
ctx.fillStyle = '#ffc832'; ctx.fill();
// Control points
[P0, P1, P2].forEach(function(p) {
ctx.beginPath(); ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);
ctx.fillStyle = '#64c8ff'; ctx.fill();
});
ctx.fillStyle = '#fff'; ctx.font = '14px monospace';
ctx.fillText('Quadratic Bézier — t = ' + t.toFixed(2), 20, 30);
t += dir * 0.006;
if (t >= 1) { t = 1; dir = -1; }
if (t <= 0) { t = 0; dir = 1; }
requestAnimationFrame(draw);
}
draw();
3. Cubic Bézier curve
The cubic Bézier is the workhorse of computer graphics. With four control points (start, two control handles, end), it can represent an enormous variety of smooth shapes. CSS cubic-bezier() timing functions, SVG path C commands, Photoshop pen tool segments, and font glyph outlines — all cubic Bézier. The formula is three nested levels of lerp: lerp between three quadratic sub-curves, which themselves lerp between linear segments.
var canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var t = 0, dir = 1;
var P0 = { x: 50, y: 350 };
var P1 = { x: 150, y: 50 };
var P2 = { x: 450, y: 50 };
var P3 = { x: 550, y: 350 };
function lerp(a, b, t) { return a + (b - a) * t; }
function cubicBezier(p0, p1, p2, p3, t) {
var x01 = lerp(p0.x, p1.x, t), y01 = lerp(p0.y, p1.y, t);
var x12 = lerp(p1.x, p2.x, t), y12 = lerp(p1.y, p2.y, t);
var x23 = lerp(p2.x, p3.x, t), y23 = lerp(p2.y, p3.y, t);
var x012 = lerp(x01, x12, t), y012 = lerp(y01, y12, t);
var x123 = lerp(x12, x23, t), y123 = lerp(y12, y23, t);
return { x: lerp(x012, x123, t), y: lerp(y012, y123, t) };
}
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 400);
// Control polygon
ctx.strokeStyle = 'rgba(100, 200, 255, 0.25)';
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(P0.x, P0.y);
ctx.lineTo(P1.x, P1.y);
ctx.lineTo(P2.x, P2.y);
ctx.lineTo(P3.x, P3.y);
ctx.stroke();
ctx.setLineDash([]);
// Construction lines at current t
var x01 = lerp(P0.x, P1.x, t), y01 = lerp(P0.y, P1.y, t);
var x12 = lerp(P1.x, P2.x, t), y12 = lerp(P1.y, P2.y, t);
var x23 = lerp(P2.x, P3.x, t), y23 = lerp(P2.y, P3.y, t);
var x012 = lerp(x01, x12, t), y012 = lerp(y01, y12, t);
var x123 = lerp(x12, x23, t), y123 = lerp(y12, y23, t);
// Level 1 lines (green)
ctx.strokeStyle = 'rgba(100, 255, 150, 0.4)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(x01, y01); ctx.lineTo(x12, y12); ctx.lineTo(x23, y23);
ctx.stroke();
// Level 2 line (yellow)
ctx.strokeStyle = 'rgba(255, 200, 50, 0.5)';
ctx.beginPath();
ctx.moveTo(x012, y012); ctx.lineTo(x123, y123);
ctx.stroke();
// Full curve
ctx.strokeStyle = '#ff6b9d';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(P0.x, P0.y);
for (var i = 1; i <= 120; i++) {
var pt = cubicBezier(P0, P1, P2, P3, i / 120);
ctx.lineTo(pt.x, pt.y);
}
ctx.stroke();
// Point on curve
var cur = cubicBezier(P0, P1, P2, P3, t);
ctx.beginPath(); ctx.arc(cur.x, cur.y, 9, 0, Math.PI * 2);
ctx.fillStyle = '#ff6b9d'; ctx.fill();
// Intermediate points
[[x01,y01],[x12,y12],[x23,y23]].forEach(function(p) {
ctx.beginPath(); ctx.arc(p[0], p[1], 4, 0, Math.PI * 2);
ctx.fillStyle = '#64ff96'; ctx.fill();
});
[[x012,y012],[x123,y123]].forEach(function(p) {
ctx.beginPath(); ctx.arc(p[0], p[1], 5, 0, Math.PI * 2);
ctx.fillStyle = '#ffc832'; ctx.fill();
});
// Control points
[P0, P1, P2, P3].forEach(function(p, i) {
ctx.beginPath(); ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);
ctx.fillStyle = '#64c8ff'; ctx.fill();
ctx.fillStyle = '#fff'; ctx.font = '12px monospace';
ctx.fillText('P' + i, p.x + 10, p.y - 10);
});
ctx.fillStyle = '#fff'; ctx.font = '14px monospace';
ctx.fillText('Cubic Bézier — t = ' + t.toFixed(2), 20, 30);
t += dir * 0.005;
if (t >= 1) { t = 1; dir = -1; }
if (t <= 0) { t = 0; dir = 1; }
requestAnimationFrame(draw);
}
draw();
4. De Casteljau's algorithm visualized
De Casteljau's algorithm is the geometric construction behind Bézier curves. It works for any degree: given N control points, recursively lerp adjacent pairs until one point remains. This visualization shows the full recursive subdivision tree for a degree-5 curve (6 control points), revealing the fractal beauty of the algorithm. Each colored layer represents one level of recursion.
var canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var t = 0, dir = 1;
var points = [
{ x: 40, y: 350 }, { x: 120, y: 80 }, { x: 250, y: 380 },
{ x: 380, y: 60 }, { x: 480, y: 300 }, { x: 560, y: 100 }
];
var colors = ['#64c8ff', '#64ff96', '#ffc832', '#ff6b9d', '#c896ff'];
function lerp(a, b, t) { return a + (b - a) * t; }
function deCasteljau(pts, t) {
var levels = [pts];
var current = pts;
while (current.length > 1) {
var next = [];
for (var i = 0; i < current.length - 1; i++) {
next.push({ x: lerp(current[i].x, current[i+1].x, t),
y: lerp(current[i].y, current[i+1].y, t) });
}
levels.push(next);
current = next;
}
return levels;
}
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 400);
var levels = deCasteljau(points, t);
// Draw full curve
ctx.strokeStyle = '#ff6b9d';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (var i = 1; i <= 150; i++) {
var lv = deCasteljau(points, i / 150);
var last = lv[lv.length - 1][0];
ctx.lineTo(last.x, last.y);
}
ctx.stroke();
// Draw each level's connections and points
for (var l = 0; l < levels.length; l++) {
var col = colors[l % colors.length];
var pts = levels[l];
if (pts.length > 1) {
ctx.strokeStyle = col.replace(')', ', 0.4)').replace('rgb', 'rgba').replace('#', '');
ctx.strokeStyle = col + '66';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (var j = 1; j < pts.length; j++) ctx.lineTo(pts[j].x, pts[j].y);
ctx.stroke();
}
var r = l === 0 ? 5 : l === levels.length - 1 ? 9 : 3.5;
pts.forEach(function(p) {
ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
ctx.fillStyle = l === levels.length - 1 ? '#ff6b9d' : col;
ctx.fill();
});
}
ctx.fillStyle = '#fff'; ctx.font = '14px monospace';
ctx.fillText('De Casteljau (degree 5) — t = ' + t.toFixed(2), 20, 30);
t += dir * 0.004;
if (t >= 1) { t = 1; dir = -1; }
if (t <= 0) { t = 0; dir = 1; }
requestAnimationFrame(draw);
}
draw();
5. Interactive curve editor
The best way to build intuition for Bézier curves is to drag control points around and see how the curve responds. This interactive editor lets you click and drag any of the four cubic control points. Notice how P1 and P2 (the handles) act like magnets — the curve is "pulled" toward them but doesn't pass through them. This is exactly how the Pen tool works in Illustrator, Figma, and every vector design tool.
var canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var pts = [
{ x: 60, y: 320 }, { x: 180, y: 60 },
{ x: 420, y: 60 }, { x: 540, y: 320 }
];
var dragging = -1;
function lerp(a, b, t) { return a + (b - a) * t; }
function cubicPt(t) {
var a = pts[0], b = pts[1], c = pts[2], d = pts[3];
var x01 = lerp(a.x,b.x,t), y01 = lerp(a.y,b.y,t);
var x12 = lerp(b.x,c.x,t), y12 = lerp(b.y,c.y,t);
var x23 = lerp(c.x,d.x,t), y23 = lerp(c.y,d.y,t);
var x012 = lerp(x01,x12,t), y012 = lerp(y01,y12,t);
var x123 = lerp(x12,x23,t), y123 = lerp(y12,y23,t);
return { x: lerp(x012,x123,t), y: lerp(y012,y123,t) };
}
canvas.addEventListener('mousedown', function(e) {
var r = canvas.getBoundingClientRect();
var mx = e.clientX - r.left, my = e.clientY - r.top;
pts.forEach(function(p, i) {
if (Math.hypot(p.x - mx, p.y - my) < 15) dragging = i;
});
});
canvas.addEventListener('mousemove', function(e) {
if (dragging < 0) return;
var r = canvas.getBoundingClientRect();
pts[dragging].x = e.clientX - r.left;
pts[dragging].y = e.clientY - r.top;
});
canvas.addEventListener('mouseup', function() { dragging = -1; });
function draw() {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 400);
// Handle lines
ctx.strokeStyle = 'rgba(100, 200, 255, 0.3)';
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y); ctx.lineTo(pts[1].x, pts[1].y);
ctx.moveTo(pts[2].x, pts[2].y); ctx.lineTo(pts[3].x, pts[3].y);
ctx.stroke();
ctx.setLineDash([]);
// Curve
ctx.strokeStyle = '#ff6b9d';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (var i = 1; i <= 100; i++) {
var p = cubicPt(i / 100);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
// Evenly-spaced dots along curve
for (var i = 0; i <= 20; i++) {
var p = cubicPt(i / 20);
ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 107, 157, 0.5)'; ctx.fill();
}
// Control points
pts.forEach(function(p, i) {
ctx.beginPath(); ctx.arc(p.x, p.y, i === 0 || i === 3 ? 8 : 6, 0, Math.PI * 2);
ctx.fillStyle = i === 0 || i === 3 ? '#64c8ff' : '#ffc832';
ctx.fill();
ctx.fillStyle = '#fff'; ctx.font = '11px monospace';
ctx.fillText('P' + i, p.x + 12, p.y - 8);
});
ctx.fillStyle = '#fff'; ctx.font = '14px monospace';
ctx.fillText('Drag any point — Pen Tool physics', 20, 30);
requestAnimationFrame(draw);
}
draw();
6. Smooth spline path (chained cubic segments)
A single cubic Bézier can only represent limited shapes. For complex curves — a handwritten letter, a country border, a racing line — you chain multiple cubic segments end-to-end with C1 continuity (matching tangent directions at joints). This example draws a smooth spline through a set of waypoints by automatically computing control handles from neighboring points, the same technique used in Catmull-Rom splines and smooth polyline fitting.
var canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var time = 0;
// Generate a set of waypoints in a flowing path
function makePoints() {
var pts = [];
for (var i = 0; i < 8; i++) {
pts.push({
x: 50 + i * 75,
y: 200 + Math.sin(i * 0.9) * 120 + Math.cos(i * 1.3) * 60
});
}
return pts;
}
var waypoints = makePoints();
function lerp(a, b, t) { return a + (b - a) * t; }
// Compute smooth cubic Bézier control points for a spline
function splineControls(pts) {
var cps = [];
for (var i = 0; i < pts.length; i++) {
var prev = pts[i - 1] || pts[i];
var curr = pts[i];
var next = pts[i + 1] || pts[i];
var dx = next.x - prev.x, dy = next.y - prev.y;
cps.push({
cp1: { x: curr.x - dx * 0.2, y: curr.y - dy * 0.2 },
cp2: { x: curr.x + dx * 0.2, y: curr.y + dy * 0.2 }
});
}
return cps;
}
function cubicPt(p0, p1, p2, p3, t) {
var x01 = lerp(p0.x,p1.x,t), y01 = lerp(p0.y,p1.y,t);
var x12 = lerp(p1.x,p2.x,t), y12 = lerp(p1.y,p2.y,t);
var x23 = lerp(p2.x,p3.x,t), y23 = lerp(p2.y,p3.y,t);
var x012 = lerp(x01,x12,t), y012 = lerp(y01,y12,t);
var x123 = lerp(x12,x23,t), y123 = lerp(y12,y23,t);
return { x: lerp(x012,x123,t), y: lerp(y012,y123,t) };
}
function draw() {
time += 0.015;
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 400);
// Animate waypoints gently
for (var i = 0; i < waypoints.length; i++) {
waypoints[i].y = 200 + Math.sin(i * 0.9 + time) * 120 + Math.cos(i * 1.3 + time * 0.7) * 60;
}
var cps = splineControls(waypoints);
// Draw full spline
ctx.strokeStyle = '#ff6b9d';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(waypoints[0].x, waypoints[0].y);
for (var i = 0; i < waypoints.length - 1; i++) {
var a = waypoints[i], b = waypoints[i + 1];
var c1 = cps[i].cp2, c2 = cps[i + 1].cp1;
for (var s = 1; s <= 30; s++) {
var p = cubicPt(a, c1, c2, b, s / 30);
ctx.lineTo(p.x, p.y);
}
}
ctx.stroke();
// Draw control handles
for (var i = 0; i < waypoints.length; i++) {
var w = waypoints[i], cp = cps[i];
ctx.strokeStyle = 'rgba(100, 200, 255, 0.2)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cp.cp1.x, cp.cp1.y);
ctx.lineTo(w.x, w.y);
ctx.lineTo(cp.cp2.x, cp.cp2.y);
ctx.stroke();
ctx.beginPath(); ctx.arc(cp.cp1.x, cp.cp1.y, 3, 0, Math.PI * 2);
ctx.fillStyle = '#ffc832'; ctx.fill();
ctx.beginPath(); ctx.arc(cp.cp2.x, cp.cp2.y, 3, 0, Math.PI * 2);
ctx.fillStyle = '#ffc832'; ctx.fill();
}
// Waypoints
waypoints.forEach(function(p) {
ctx.beginPath(); ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
ctx.fillStyle = '#64c8ff'; ctx.fill();
});
ctx.fillStyle = '#fff'; ctx.font = '14px monospace';
ctx.fillText('Animated Catmull-Rom spline (7 segments)', 20, 30);
requestAnimationFrame(draw);
}
draw();
7. Text along a Bézier path
One of the classic applications of Bézier curves is placing text along a curved path — think logos, stamps, and decorative typography. The technique: sample points along the curve at regular arc-length intervals, compute the tangent angle at each point, and draw each character rotated to follow the curve. This is how Illustrator's "Type on a Path" tool works internally.
var canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var time = 0;
function lerp(a, b, t) { return a + (b - a) * t; }
function cubicPt(p0, p1, p2, p3, t) {
var x01 = lerp(p0.x,p1.x,t), y01 = lerp(p0.y,p1.y,t);
var x12 = lerp(p1.x,p2.x,t), y12 = lerp(p1.y,p2.y,t);
var x23 = lerp(p2.x,p3.x,t), y23 = lerp(p2.y,p3.y,t);
var x012 = lerp(x01,x12,t), y012 = lerp(y01,y12,t);
var x123 = lerp(x12,x23,t), y123 = lerp(y12,y23,t);
return { x: lerp(x012,x123,t), y: lerp(y012,y123,t) };
}
function tangentAt(p0, p1, p2, p3, t) {
var dt = 0.001;
var a = cubicPt(p0, p1, p2, p3, Math.max(0, t - dt));
var b = cubicPt(p0, p1, p2, p3, Math.min(1, t + dt));
return Math.atan2(b.y - a.y, b.x - a.x);
}
var text = 'Bézier curves are everywhere in design ';
function draw() {
time += 0.008;
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 400);
// Animated control points
var P0 = { x: 40, y: 300 };
var P1 = { x: 150 + Math.sin(time) * 60, y: 60 + Math.cos(time * 0.7) * 40 };
var P2 = { x: 450 + Math.cos(time * 0.8) * 60, y: 60 + Math.sin(time * 0.6) * 40 };
var P3 = { x: 560, y: 300 };
// Draw curve path (faint)
ctx.strokeStyle = 'rgba(255, 107, 157, 0.2)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(P0.x, P0.y);
for (var i = 1; i <= 100; i++) {
var p = cubicPt(P0, P1, P2, P3, i / 100);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
// Place text along curve
ctx.font = 'bold 20px Georgia, serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
var chars = text.split('');
var step = 1 / chars.length;
for (var i = 0; i < chars.length; i++) {
var t = (i * step + time * 0.1) % 1;
var pt = cubicPt(P0, P1, P2, P3, t);
var angle = tangentAt(P0, P1, P2, P3, t);
ctx.save();
ctx.translate(pt.x, pt.y);
ctx.rotate(angle);
var hue = (i / chars.length) * 360;
ctx.fillStyle = 'hsl(' + hue + ', 80%, 65%)';
ctx.fillText(chars[i], 0, 0);
ctx.restore();
}
// Control points
[P0, P1, P2, P3].forEach(function(p) {
ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(100, 200, 255, 0.4)'; ctx.fill();
});
ctx.fillStyle = '#fff'; ctx.font = '14px monospace';
ctx.fillText('Text following animated Bézier path', 20, 30);
requestAnimationFrame(draw);
}
draw();
8. Generative Bézier art
When you randomize control points and chain dozens of Bézier curves together with varying opacity and color, you get something magical: organic, flowing shapes that look hand-drawn. This technique is used in generative art, data sonification visualizations, and abstract backgrounds. This example generates a continuous stream of overlapping cubic curves with HSL color cycling, creating a living canvas of flowing lines.
var canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var time = 0;
var curves = [];
function lerp(a, b, t) { return a + (b - a) * t; }
function cubicPt(p0, p1, p2, p3, t) {
var x01 = lerp(p0.x,p1.x,t), y01 = lerp(p0.y,p1.y,t);
var x12 = lerp(p1.x,p2.x,t), y12 = lerp(p1.y,p2.y,t);
var x23 = lerp(p2.x,p3.x,t), y23 = lerp(p2.y,p3.y,t);
var x012 = lerp(x01,x12,t), y012 = lerp(y01,y12,t);
var x123 = lerp(x12,x23,t), y123 = lerp(y12,y23,t);
return { x: lerp(x012,x123,t), y: lerp(y012,y123,t) };
}
// Seed initial curves
for (var i = 0; i < 30; i++) {
curves.push({
p0: { x: Math.random() * 600, y: Math.random() * 400 },
p1: { x: Math.random() * 600, y: Math.random() * 400 },
p2: { x: Math.random() * 600, y: Math.random() * 400 },
p3: { x: Math.random() * 600, y: Math.random() * 400 },
hue: Math.random() * 360,
speed: 0.3 + Math.random() * 0.7,
width: 1 + Math.random() * 3
});
}
function draw() {
time += 0.01;
// Slow fade for trail effect
ctx.fillStyle = 'rgba(10, 10, 26, 0.04)';
ctx.fillRect(0, 0, 600, 400);
curves.forEach(function(c) {
// Animate control points with sine waves
var ox = Math.sin(time * c.speed) * 80;
var oy = Math.cos(time * c.speed * 0.7) * 60;
var p0 = { x: c.p0.x + ox * 0.3, y: c.p0.y + oy * 0.3 };
var p1 = { x: c.p1.x + oy * 0.6, y: c.p1.y - ox * 0.5 };
var p2 = { x: c.p2.x - ox * 0.5, y: c.p2.y + oy * 0.6 };
var p3 = { x: c.p3.x + ox * 0.3, y: c.p3.y - oy * 0.3 };
ctx.strokeStyle = 'hsla(' + ((c.hue + time * 20) % 360) + ', 70%, 60%, 0.15)';
ctx.lineWidth = c.width;
ctx.beginPath();
ctx.moveTo(p0.x, p0.y);
for (var s = 1; s <= 60; s++) {
var pt = cubicPt(p0, p1, p2, p3, s / 60);
ctx.lineTo(pt.x, pt.y);
}
ctx.stroke();
});
ctx.fillStyle = '#fff'; ctx.font = '14px monospace';
ctx.fillText('Generative Bézier art — 30 animated curves', 20, 30);
requestAnimationFrame(draw);
}
// Initial black fill
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 600, 400);
draw();
The mathematics behind Bézier curves
The closed-form equation for a Bézier curve of degree n uses Bernstein basis polynomials:
B(t) = Σ (n choose i) · (1-t)^(n-i) · t^i · P_i
For a cubic Bézier (n=3), this expands to:
B(t) = (1-t)³·P0 + 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³·P3
But De Casteljau's recursive lerp approach is more numerically stable and easier to understand. Both give identical results — the difference is computational: Bernstein is faster for single-point evaluation, De Casteljau is better for subdivision and is more resistant to floating-point error.
Bézier curves in practice
Bézier curves are everywhere in modern computing:
- Typography: TrueType fonts use quadratic Béziers; OpenType/CFF fonts use cubic Béziers. Every letter you're reading now is made of Bézier curves.
- SVG and Canvas: The
quadraticCurveTo()andbezierCurveTo()Canvas methods, and SVG'sQ,C, andSpath commands are all Bézier curves. - CSS animations:
cubic-bezier(0.4, 0, 0.2, 1)defines easing curves for transitions — the control points map to velocity over time. - CAD/CAM: Car bodies, aircraft fuselages, and industrial design use NURBS (Non-Uniform Rational B-Splines), which generalize Bézier curves with weights and knot vectors.
- Game development: Camera paths, enemy patrol routes, particle trajectories, and UI animations all use Bézier curves for smooth, controllable motion.
- SVG animation: Motion paths in SVG and Lottie/After Effects keyframe interpolation are cubic Bézier segments.
Going further
- Explore mathematical art for more curve-based generative techniques including Lissajous figures, rose curves, and spirographs
- Learn about SVG animation to see Bézier curves in action for web animations and motion graphics
- Try drawing with code for a broader introduction to programmatic art with Canvas and WebGL
- Combine Bézier curves with Perlin noise to create organic, hand-drawn-looking curves by jittering control points with noise values
- Use rational Bézier curves (weighted control points) to represent perfect circles and ellipses — standard Béziers can only approximate these
- Experiment with Bézier surfaces (tensor products of curves) for 3D surface modeling — the Utah teapot was famously defined using Bézier patches
- On Lumitree, several micro-worlds use Bézier curves to draw flowing organic branches, animated vines, and smooth particle trails — every visitor's seed grows along curves that blend mathematical precision with natural beauty