贝塞尔曲线

引言

贝塞尔曲线(Bézier Curve)是现代图形设计中定义平滑曲线的核心工具,由法国数学家皮埃尔·贝塞尔(Pierre Bézier)在20世纪60年代用于汽车车身设计,如今已广泛应用于UI设计、动画路径、字体渲染等领域。

在Canvas中,我们可以通过原生API轻松绘制2阶和3阶贝塞尔曲线,甚至可以自行实现更高阶的曲线。本文将从原理到实战,带你彻底掌握Canvas中的贝塞尔曲线。


一、2阶贝塞尔曲线(Quadratic Bézier Curve)

1. 原理

2阶贝塞尔曲线是最简单的贝塞尔曲线,由1个起点(P0)1个控制点(P1)1个终点(P2)定义:

  • 曲线从P0出发,最终到达P2;
  • 曲线在P0处与线段P0→P1相切,在P2处与线段P1→P2相切;
  • 可以直观理解为:一根橡皮筋从P0拉到P2,中间被P1“顶”出一个平滑的弧度。

2. API语法

1
ctx.quadraticCurveTo(cp1x, cp1y, x, y);

参数说明:

  • cp1x, cp1y:控制点(P1)的坐标;
  • x, y:终点(P2)的坐标;
  • ⚠️ 注意:起点(P0)由上一个moveTo()或路径点决定,无需在该方法中传入。

3. 代码示例(含辅助线)

为了更直观地理解曲线与控制点的关系,我们可以绘制辅助线(起点-控制点、控制点-终点的虚线):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// 1. 定义关键点
const p0 = { x: 20, y: 110 }; // 起点
const p1 = { x: 230, y: 150 }; // 控制点
const p2 = { x: 250, y: 20 }; // 终点

// 2. 绘制辅助线(虚线)
ctx.beginPath();
ctx.setLineDash([5, 5]); // 设置虚线样式
ctx.strokeStyle = '#999';
ctx.moveTo(p0.x, p0.y);
ctx.lineTo(p1.x, p1.y); // 起点→控制点
ctx.lineTo(p2.x, p2.y); // 控制点→终点
ctx.stroke();

// 3. 绘制关键点(实心圆)
ctx.beginPath();
ctx.setLineDash([]); // 恢复实线
ctx.fillStyle = '#ff6b6b';
[ p0, p1, p2 ].forEach(point => {
ctx.moveTo(point.x + 5, point.y);
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
});
ctx.fill();

// 4. 绘制2阶贝塞尔曲线
ctx.beginPath();
ctx.strokeStyle = '#1890ff';
ctx.lineWidth = 2;
ctx.moveTo(p0.x, p0.y); // 设置起点
ctx.quadraticCurveTo(p1.x, p1.y, p2.x, p2.y); // 绘制曲线
ctx.stroke();

4. 应用场景

  • 绘制抛物线(如投篮轨迹、物体下落曲线);
  • 简单的圆角过渡(如按钮、卡片的圆角);
  • 气泡对话框的尖角与主体的平滑连接。

二、3阶贝塞尔曲线(Cubic Bézier Curve)

1. 原理

3阶贝塞尔曲线是最常用的贝塞尔曲线,由1个起点(P0)2个控制点(P1、P2)1个终点(P3)定义:

  • 曲线在P0处与线段P0→P1相切,在P3处与线段P2→P3相切;
  • 两个控制点分别控制曲线起点和终点的“弯曲方向”,比2阶曲线更灵活,能实现更复杂的平滑效果。

2. API语法

1
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);

参数说明:

  • cp1x, cp1y:第一个控制点(P1)的坐标;
  • cp2x, cp2y:第二个控制点(P2)的坐标;
  • x, y:终点(P3)的坐标。

3. 代码示例(含辅助线)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// 1. 定义关键点
const p0 = { x: 50, y: 20 }; // 起点
const p1 = { x: 230, y: 30 }; // 控制点1
const p2 = { x: 150, y: 60 }; // 控制点2
const p3 = { x: 50, y: 100 }; // 终点

// 2. 绘制辅助线(虚线)
ctx.beginPath();
ctx.setLineDash([5, 5]);
ctx.strokeStyle = '#999';
ctx.moveTo(p0.x, p0.y);
ctx.lineTo(p1.x, p1.y); // 起点→控制点1
ctx.moveTo(p2.x, p2.y);
ctx.lineTo(p3.x, p3.y); // 控制点2→终点
ctx.stroke();

// 3. 绘制关键点
ctx.beginPath();
ctx.setLineDash([]);
ctx.fillStyle = '#ff6b6b';
[ p0, p1, p2, p3 ].forEach(point => {
ctx.moveTo(point.x + 5, point.y);
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
});
ctx.fill();

// 4. 绘制3阶贝塞尔曲线
ctx.beginPath();
ctx.strokeStyle = '#1890ff';
ctx.lineWidth = 2;
ctx.moveTo(p0.x, p0.y);
ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
ctx.stroke();

4. 应用场景

  • 字体轮廓渲染(如TrueType、OpenType字体);
  • 复杂图标和插画的平滑曲线;
  • 动画缓动路径(结合CSS cubic-bezier() 或Canvas动画);
  • 自然形态绘制(如河流、头发、植物藤蔓)。

三、N阶贝塞尔曲线:自定义实现

Canvas原生仅支持2阶和3阶贝塞尔曲线,若需要更高阶的曲线(如4阶、5阶),可以通过德卡斯特里奥算法(De Casteljau’s Algorithm)自行实现。

1. 德卡斯特里奥算法原理

该算法的核心是多次线性插值

  • 对于n个控制点,先在每两个相邻控制点之间进行线性插值,得到n-1个新点;
  • 对新点重复上述过程,直到剩下1个点,该点即为贝塞尔曲线上的点;
  • 遍历参数t(0 ≤ t ≤ 1),即可得到完整的曲线。

2. 简单实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 绘制N阶贝塞尔曲线
* @param {CanvasRenderingContext2D} ctx - Canvas上下文
* @param {Array<{x: number, y: number}>} points - 控制点数组(包含起点和终点)
* @param {number} [step=0.01] - 插值步长(越小越平滑)
*/
function drawN阶Bezier(ctx, points, step = 0.01) {
ctx.beginPath();

for (let t = 0; t <= 1; t += step) {
// 复制控制点数组,避免修改原数据
let tempPoints = [...points];

// 德卡斯特里奥算法:多次线性插值
while (tempPoints.length > 1) {
const newPoints = [];
for (let i = 0; i < tempPoints.length - 1; i++) {
const p0 = tempPoints[i];
const p1 = tempPoints[i + 1];
// 线性插值:p = p0 + t * (p1 - p0)
newPoints.push({
x: p0.x + t * (p1.x - p0.x),
y: p0.y + t * (p1.y - p0.y)
});
}
tempPoints = newPoints;
}

// 连接曲线上的点
const point = tempPoints[0];
if (t === 0) {
ctx.moveTo(point.x, point.y);
} else {
ctx.lineTo(point.x, point.y);
}
}

ctx.stroke();
}

// 使用示例
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#1890ff';
ctx.lineWidth = 2;

// 定义5个控制点(4阶贝塞尔曲线)
const controlPoints = [
{ x: 50, y: 200 },
{ x: 100, y: 50 },
{ x: 200, y: 300 },
{ x: 300, y: 100 },
{ x: 350, y: 200 }
];

drawN阶Bezier(ctx, controlPoints);

3. 推荐工具库

如果不想手动实现算法,可以使用开源库简化开发:

  • bezierMaker.jsGitHub仓库,支持可视化调整N阶贝塞尔曲线的控制点。

四、实战技巧与注意事项

1. 辅助线调试

绘制曲线时,建议先画出控制点和辅助线(如本文示例中的虚线和实心圆),方便快速调整曲线形状,调试完成后再隐藏辅助线。

2. 性能优化

  • 复杂场景下,优先使用3阶贝塞尔曲线而非多段2阶曲线,前者更平滑且计算量更小;
  • 若需要重复绘制相同的贝塞尔曲线,可以使用Path2D对象缓存路径:
    1
    2
    3
    4
    5
    const path = new Path2D();
    path.moveTo(50, 20);
    path.bezierCurveTo(230, 30, 150, 60, 50, 100);
    // 后续直接绘制缓存的路径
    ctx.stroke(path);

3. 与SVG结合

SVG的<path>标签也支持贝塞尔曲线(Q/q表示2阶,C/c表示3阶),可以先用设计工具(如Figma、Sketch)导出SVG路径,再转换为Canvas代码使用。


五、拓展阅读


小结

贝塞尔曲线是Canvas绘图的“瑞士军刀”:

  • 2阶曲线适合简单平滑过渡;
  • 3阶曲线灵活度最高,是最常用的选择;
  • N阶曲线可通过德卡斯特里奥算法实现,用于特殊复杂场景。

通过辅助线调试和工具库辅助,你可以快速将贝塞尔曲线应用到实际项目中,创造出流畅自然的图形效果!


贝塞尔曲线
https://cszy.top/20230331-贝塞尔曲线/
作者
csorz
发布于
2023年3月31日
许可协议