pin_drop当前位置:知识文库 ❯ 图文
多媒体:贝塞尔曲线 - 完整教程与代码示例
一、教程简介
贝塞尔曲线是计算机图形学中最常用的曲线类型,广泛应用于矢量绘图、字体设计、动画路径等领域。Canvas 提供了两种贝塞尔曲线绘制方法:quadraticCurveTo 绘制二次贝塞尔曲线(1个控制点),bezierCurveTo 绘制三次贝塞尔曲线(2个控制点)。本教程将详细讲解贝塞尔曲线的原理、绘制方法和实际应用。
二、核心概念
二次贝塞尔曲线(Quadratic Bezier)
二次贝塞尔曲线由三个点定义:
起点(P0):当前路径点
控制点(P1):决定曲线弯曲方向和程度
终点(P2):曲线结束点
曲线上的点通过参数 t(0~1)计算:
代码示例
B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2三次贝塞尔曲线(Cubic Bezier)
三次贝塞尔曲线由四个点定义:
起点(P0):当前路径点
控制点1(P1):影响曲线起始方向
控制点2(P2):影响曲线结束方向
终点(P3):曲线结束点
曲线上的点通过参数 t(0~1)计算:
代码示例
B(t) = (1-t)^3 * P0 + 3(1-t)^2t * P1 + 3(1-t)t^2 * P2 + t^3 * P3控制点的作用
控制点不在曲线上,但它们像磁铁一样"吸引"曲线,控制曲线的弯曲方向和程度。控制点离曲线越远,曲线弯曲越明显。
三、语法与用法
quadraticCurveTo - 二次贝塞尔曲线
代码示例
ctx.quadraticCurveTo(cpx, cpy, x, y);起点为当前路径点。
bezierCurveTo - 三次贝塞尔曲线
代码示例
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);起点为当前路径点。
四、代码示例
示例1:贝塞尔曲线原理与对比
以下代码演示了二次贝塞尔曲线和三次贝塞尔曲线的原理对比,以及控制点位置对曲线形状的影响:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贝塞尔曲线原理与对比</title>
<style>
body {
margin: 0;
padding: 20px;
background: #0d1117;
color: #c9d1d9;
font-family: 'Segoe UI', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
border: 1px solid #30363d;
border-radius: 8px;
background: #161b22;
}
</style>
</head>
<body>
<h1>贝塞尔曲线原理与对比</h1>
<canvas id="myCanvas" width="750" height="500"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// === 二次贝塞尔曲线 ===
ctx.fillStyle = '#c9d1d9';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('二次贝塞尔曲线 (quadraticCurveTo)', 200, 25);
const q = {
p0: { x: 50, y: 200 },
cp: { x: 200, y: 50 },
p2: { x: 350, y: 200 }
};
// 辅助线
ctx.beginPath();
ctx.moveTo(q.p0.x, q.p0.y);
ctx.lineTo(q.cp.x, q.cp.y);
ctx.lineTo(q.p2.x, q.p2.y);
ctx.strokeStyle = '#30363d';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.setLineDash([]);
// 曲线
ctx.beginPath();
ctx.moveTo(q.p0.x, q.p0.y);
ctx.quadraticCurveTo(q.cp.x, q.cp.y, q.p2.x, q.p2.y);
ctx.strokeStyle = '#58a6ff';
ctx.lineWidth = 3;
ctx.stroke();
// 标注点
[[q.p0, 'P0 起点'], [q.cp, 'P1 控制点'], [q.p2, 'P2 终点']].forEach(([p, label]) => {
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
ctx.fillStyle = label.includes('控制') ? '#e94560' : '#3fb950';
ctx.fill();
ctx.fillStyle = '#8b949e';
ctx.font = '11px sans-serif';
ctx.fillText(label, p.x, p.y - 15);
});
// 绘制 t 值动画点
for (let t = 0; t <= 1; t += 0.1) {
const x = (1-t)*(1-t)*q.p0.x + 2*(1-t)*t*q.cp.x + t*t*q.p2.x;
const y = (1-t)*(1-t)*q.p0.y + 2*(1-t)*t*q.cp.y + t*t*q.p2.y;
ctx.beginPath();
ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fillStyle = '#58a6ff';
ctx.fill();
}
// === 三次贝塞尔曲线 ===
ctx.fillStyle = '#c9d1d9';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('三次贝塞尔曲线 (bezierCurveTo)', 575, 25);
const c = {
p0: { x: 425, y: 200 },
cp1: { x: 475, y: 50 },
cp2: { x: 675, y: 350 },
p3: { x: 725, y: 200 }
};
// 辅助线
ctx.beginPath();
ctx.moveTo(c.p0.x, c.p0.y);
ctx.lineTo(c.cp1.x, c.cp1.y);
ctx.moveTo(c.cp2.x, c.cp2.y);
ctx.lineTo(c.p3.x, c.p3.y);
ctx.strokeStyle = '#30363d';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.setLineDash([]);
// 曲线
ctx.beginPath();
ctx.moveTo(c.p0.x, c.p0.y);
ctx.bezierCurveTo(c.cp1.x, c.cp1.y, c.cp2.x, c.cp2.y, c.p3.x, c.p3.y);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 3;
ctx.stroke();
// 标注点
[[c.p0, 'P0 起点'], [c.cp1, 'P1 控制点1'], [c.cp2, 'P2 控制点2'], [c.p3, 'P3 终点']].forEach(([p, label]) => {
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
ctx.fillStyle = label.includes('控制') ? '#e94560' : '#3fb950';
ctx.fill();
ctx.fillStyle = '#8b949e';
ctx.font = '11px sans-serif';
ctx.fillText(label, p.x, p.y - 15);
});
// 绘制 t 值动画点
for (let t = 0; t <= 1; t += 0.05) {
const mt = 1 - t;
const x = mt*mt*mt*c.p0.x + 3*mt*mt*t*c.cp1.x + 3*mt*t*t*c.cp2.x + t*t*t*c.p3.x;
const y = mt*mt*mt*c.p0.y + 3*mt*mt*t*c.cp1.y + 3*mt*t*t*c.cp2.y + t*t*t*c.p3.y;
ctx.beginPath();
ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fillStyle = '#3fb950';
ctx.fill();
}
// === 不同控制点位置的效果 ===
ctx.fillStyle = '#c9d1d9';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('控制点位置对曲线的影响', 375, 260);
// 二次贝塞尔 - 控制点高低
const heights = [80, 130, 180, 230];
const colors = ['#58a6ff', '#3fb950', '#f8b500', '#e94560'];
heights.forEach((h, i) => {
ctx.beginPath();
ctx.moveTo(50, 390);
ctx.quadraticCurveTo(200, h, 350, 390);
ctx.strokeStyle = colors[i];
ctx.lineWidth = 2;
ctx.stroke();
// 控制点标记
ctx.beginPath();
ctx.arc(200, h, 3, 0, Math.PI * 2);
ctx.fillStyle = colors[i];
ctx.fill();
});
ctx.fillStyle = '#8b949e';
ctx.font = '11px sans-serif';
ctx.fillText('控制点越高曲线弯曲越大', 200, 470);
// 三次贝塞尔 - S 形曲线
ctx.beginPath();
ctx.moveTo(420, 390);
ctx.bezierCurveTo(470, 280, 630, 480, 680, 390);
ctx.strokeStyle = '#d2a8ff';
ctx.lineWidth = 3;
ctx.stroke();
ctx.fillStyle = '#8b949e';
ctx.font = '11px sans-serif';
ctx.fillText('三次贝塞尔可绘制 S 形', 550, 470);
</script>
</body>
</html>示例2:贝塞尔曲线实际应用
以下代码展示了贝塞尔曲线在实际场景中的应用,包括心形、波浪线、水滴、平滑曲线图表和花瓣图案的绘制:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贝塞尔曲线实际应用</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
color: #eee;
font-family: 'Segoe UI', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
border: 1px solid #333;
border-radius: 8px;
background: #16213e;
}
</style>
</head>
<body>
<h1>贝塞尔曲线实际应用</h1>
<canvas id="myCanvas" width="700" height="500"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// === 1. 绘制心形 ===
ctx.fillStyle = '#eee';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('心形', 120, 30);
ctx.beginPath();
ctx.moveTo(120, 130);
ctx.bezierCurveTo(120, 90, 60, 70, 60, 110);
ctx.bezierCurveTo(60, 140, 120, 170, 120, 190);
ctx.bezierCurveTo(120, 170, 180, 140, 180, 110);
ctx.bezierCurveTo(180, 70, 120, 90, 120, 130);
ctx.fillStyle = 'rgba(233, 69, 96, 0.5)';
ctx.fill();
ctx.strokeStyle = '#e94560';
ctx.lineWidth = 2;
ctx.stroke();
// === 2. 绘制波浪线 ===
ctx.fillStyle = '#eee';
ctx.fillText('波浪线', 350, 30);
ctx.beginPath();
ctx.moveTo(230, 120);
for (let i = 0; i < 4; i++) {
const x = 230 + i * 60;
ctx.quadraticCurveTo(x + 15, 80, x + 30, 120);
ctx.quadraticCurveTo(x + 45, 160, x + 60, 120);
}
ctx.strokeStyle = '#58a6ff';
ctx.lineWidth = 3;
ctx.stroke();
// === 3. 绘制水滴 ===
ctx.fillStyle = '#eee';
ctx.fillText('水滴', 580, 30);
ctx.beginPath();
ctx.moveTo(580, 60);
ctx.quadraticCurveTo(620, 110, 620, 140);
ctx.arc(590, 140, 30, 0, Math.PI, false);
ctx.quadraticCurveTo(540, 110, 580, 60);
ctx.fillStyle = 'rgba(88, 166, 255, 0.4)';
ctx.fill();
ctx.strokeStyle = '#58a6ff';
ctx.lineWidth = 2;
ctx.stroke();
// === 4. 绘制平滑曲线图表 ===
ctx.fillStyle = '#eee';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('平滑曲线图表', 350, 220);
const dataPoints = [
{ x: 80, y: 350 }, { x: 160, y: 300 }, { x: 240, y: 320 },
{ x: 320, y: 260 }, { x: 400, y: 280 }, { x: 480, y: 240 },
{ x: 560, y: 270 }, { x: 640, y: 230 }
];
// 绘制网格
ctx.strokeStyle = '#1a2a3e';
ctx.lineWidth = 1;
for (let y = 230; y <= 370; y += 30) {
ctx.beginPath();
ctx.moveTo(60, y);
ctx.lineTo(660, y);
ctx.stroke();
}
// 绘制平滑曲线
ctx.beginPath();
ctx.moveTo(dataPoints[0].x, dataPoints[0].y);
for (let i = 0; i < dataPoints.length - 1; i++) {
const curr = dataPoints[i];
const next = dataPoints[i + 1];
const cpx = (curr.x + next.x) / 2;
ctx.quadraticCurveTo(curr.x, curr.y, cpx, (curr.y + next.y) / 2);
}
const last = dataPoints[dataPoints.length - 1];
ctx.lineTo(last.x, last.y);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 3;
ctx.stroke();
// 填充区域
ctx.lineTo(last.x, 370);
ctx.lineTo(dataPoints[0].x, 370);
ctx.closePath();
ctx.fillStyle = 'rgba(63, 185, 80, 0.1)';
ctx.fill();
// 数据点
dataPoints.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
ctx.fillStyle = '#3fb950';
ctx.fill();
ctx.strokeStyle = '#16213e';
ctx.lineWidth = 2;
ctx.stroke();
});
// === 5. 绘制花瓣 ===
ctx.fillStyle = '#eee';
ctx.font = 'bold 14px sans-serif';
ctx.fillText('花瓣图案', 350, 400);
const petalCount = 6;
const petalCx = 350, petalCy = 440;
for (let i = 0; i < petalCount; i++) {
const angle = (i / petalCount) * Math.PI * 2;
const nextAngle = ((i + 1) / petalCount) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(petalCx, petalCy);
const tipX = petalCx + 40 * Math.cos(angle - 0.3);
const tipY = petalCy + 40 * Math.sin(angle - 0.3);
const tipX2 = petalCx + 40 * Math.cos(angle + 0.3);
const tipY2 = petalCy + 40 * Math.sin(angle + 0.3);
const cp1x = petalCx + 50 * Math.cos(angle - 0.15);
const cp1y = petalCy + 50 * Math.sin(angle - 0.15);
const cp2x = petalCx + 50 * Math.cos(angle + 0.15);
const cp2y = petalCy + 50 * Math.sin(angle + 0.15);
ctx.quadraticCurveTo(cp1x, cp1y, petalCx + 55 * Math.cos(angle), petalCy + 55 * Math.sin(angle));
ctx.quadraticCurveTo(cp2x, cp2y, petalCx, petalCy);
ctx.fillStyle = `hsla(${i * 60}, 70%, 60%, 0.4)`;
ctx.fill();
ctx.strokeStyle = `hsla(${i * 60}, 70%, 60%, 0.8)`;
ctx.lineWidth = 1;
ctx.stroke();
}
// 花心
ctx.beginPath();
ctx.arc(petalCx, petalCy, 8, 0, Math.PI * 2);
ctx.fillStyle = '#f8b500';
ctx.fill();
</script>
</body>
</html>五、浏览器兼容性
六、注意事项与最佳实践
1. 起点 = 当前路径点
代码示例
// 必须先设置起点
ctx.beginPath();
ctx.moveTo(50, 100); // 起点
ctx.quadraticCurveTo(150, 50, 250, 100); // 控制点 + 终点
ctx.stroke();2. 连续绘制平滑曲线
代码示例
// 使用中点法绘制平滑曲线
function drawSmoothCurve(ctx, points) {
if (points.length < 2) return;
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
if (points.length === 2) {
ctx.lineTo(points[1][0], points[1][1]);
} else {
// 第一段使用直线到第一个中点
let midX = (points[0][0] + points[1][0]) / 2;
let midY = (points[0][1] + points[1][1]) / 2;
ctx.lineTo(midX, midY);
// 中间段使用二次贝塞尔曲线
for (let i = 1; i < points.length - 1; i++) {
const nextMidX = (points[i][0] + points[i + 1][0]) / 2;
const nextMidY = (points[i][1] + points[i + 1][1]) / 2;
ctx.quadraticCurveTo(
points[i][0], points[i][1],
nextMidX, nextMidY
);
}
// 最后一段
const last = points[points.length - 1];
ctx.lineTo(last[0], last[1]);
}
ctx.stroke();
}3. 控制点的选择
控制点越远,曲线弯曲越大
控制点方向决定曲线弯曲方向
两个控制点之间的距离影响曲线的"张力"
4. 二次 vs 三次的选择
简单弧线:使用二次贝塞尔曲线
S 形曲线:使用三次贝塞尔曲线
平滑连接:二次贝塞尔更易控制
七、代码规范示例
代码示例
// 推荐:封装二次贝塞尔曲线绘制
function drawQuadratic(ctx, startX, startY, cpX, cpY, endX, endY, options) {
const {
stroke = '#000',
fill = null,
lineWidth = 1,
showControl = false
} = options || {};
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.quadraticCurveTo(cpX, cpY, endX, endY);
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
if (stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
// 可选:显示控制点
if (showControl) {
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(cpX, cpY);
ctx.lineTo(endX, endY);
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;
ctx.stroke();
ctx.setLineDash([]);
ctx.beginPath();
ctx.arc(cpX, cpY, 4, 0, Math.PI * 2);
ctx.fillStyle = '#e94560';
ctx.fill();
}
}
// 推荐:封装三次贝塞尔曲线绘制
function drawCubic(ctx, startX, startY, cp1X, cp1Y, cp2X, cp2Y, endX, endY, options) {
const {
stroke = '#000',
fill = null,
lineWidth = 1
} = options || {};
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, endX, endY);
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
if (stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
}
// 使用示例
drawQuadratic(ctx, 50, 100, 150, 50, 250, 100, {
stroke: '#58a6ff',
lineWidth: 2,
showControl: true
});
drawCubic(ctx, 50, 200, 100, 100, 200, 300, 250, 200, {
stroke: '#3fb950',
lineWidth: 2
});八、常见问题与解决方案
Q1:如何让多段贝塞尔曲线平滑连接?
解决方案:确保连接点处的切线方向一致:
代码示例
// 方法:控制点与连接点共线
// P1 的第二个控制点、连接点、P2 的第一个控制点应在同一直线上
ctx.beginPath();
ctx.moveTo(50, 100);
ctx.bezierCurveTo(100, 50, 150, 50, 200, 100); // 第一段
ctx.bezierCurveTo(250, 150, 300, 150, 350, 100); // 第二段
// 150,50 -> 200,100 -> 250,150 大致共线,曲线平滑
ctx.stroke();Q2:如何计算贝塞尔曲线上的点?
解决方案:
代码示例
// 二次贝塞尔曲线上的点
function getQuadraticPoint(p0, p1, p2, t) {
const mt = 1 - t;
return {
x: mt * mt * p0.x + 2 * mt * t * p1.x + t * t * p2.x,
y: mt * mt * p0.y + 2 * mt * t * p1.y + t * t * p2.y
};
}
// 三次贝塞尔曲线上的点
function getCubicPoint(p0, p1, p2, p3, t) {
const mt = 1 - t;
return {
x: mt*mt*mt*p0.x + 3*mt*mt*t*p1.x + 3*mt*t*t*p2.x + t*t*t*p3.x,
y: mt*mt*mt*p0.y + 3*mt*mt*t*p1.y + 3*mt*t*t*p2.y + t*t*t*p3.y
};
}Q3:如何沿贝塞尔曲线移动物体?
解决方案:
代码示例
let t = 0;
const p0 = { x: 50, y: 200 };
const p1 = { x: 200, y: 50 };
const p2 = { x: 350, y: 200 };
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制曲线
ctx.beginPath();
ctx.moveTo(p0.x, p0.y);
ctx.quadraticCurveTo(p1.x, p1.y, p2.x, p2.y);
ctx.strokeStyle = '#58a6ff';
ctx.lineWidth = 2;
ctx.stroke();
// 计算物体位置
const pos = getQuadraticPoint(p0, p1, p2, t);
ctx.beginPath();
ctx.arc(pos.x, pos.y, 8, 0, Math.PI * 2);
ctx.fillStyle = '#e94560';
ctx.fill();
t += 0.005;
if (t > 1) t = 0;
requestAnimationFrame(animate);
}
animate();Q4:如何绘制等距的贝塞尔曲线点?
原因:参数 t 等分不等于弧长等分。
解决方案:使用近似方法或查表法:
代码示例
// 简单近似:细分 t 值,累积弧长
function getEquidistantPoints(p0, p1, p2, numPoints) {
const points = [];
const steps = 200; // 细分步数
const allPoints = [];
for (let i = 0; i <= steps; i++) {
const t = i / steps;
allPoints.push(getQuadraticPoint(p0, p1, p2, t));
}
// 计算总弧长
let totalLen = 0;
const lengths = [0];
for (let i = 1; i < allPoints.length; i++) {
const dx = allPoints[i].x - allPoints[i-1].x;
const dy = allPoints[i].y - allPoints[i-1].y;
totalLen += Math.sqrt(dx * dx + dy * dy);
lengths.push(totalLen);
}
// 等距取点
for (let i = 0; i < numPoints; i++) {
const targetLen = (i / (numPoints - 1)) * totalLen;
let idx = 0;
while (idx < lengths.length - 1 && lengths[idx + 1] < targetLen) idx++;
points.push(allPoints[idx]);
}
return points;
}Q5:二次和三次贝塞尔曲线如何选择?
解决方案:
九、总结
贝塞尔曲线是 Canvas 绘制复杂曲线的核心工具,关键要点:
quadraticCurveTo:二次贝塞尔曲线,1个控制点,适合简单弧线
bezierCurveTo:三次贝塞尔曲线,2个控制点,适合 S 形和复杂曲线
控制点:不在曲线上,决定曲线弯曲方向和程度
平滑连接:确保连接点处的控制点共线
中点法:使用二次贝塞尔曲线的中点法绘制平滑折线
参数 t:0~1 的参数控制曲线上的位置,但不等于弧长等分
实际应用:心形、波浪、水滴、图表曲线、花瓣等
下一教程将介绍如何在 Canvas 上绘制文本。
常见问题
如何让多段贝塞尔曲线平滑连接?
确保连接点处的切线方向一致,即前一段的第二个控制点、连接点、后一段的第一个控制点应在同一直线上。这样可以保证曲线在连接处的平滑过渡。
如何计算贝塞尔曲线上的点?
使用贝塞尔曲线的参数方程,通过参数 t(0~1)计算。二次贝塞尔曲线公式为 B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2,三次贝塞尔曲线公式为 B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3。
如何沿贝塞尔曲线移动物体?
使用 requestAnimationFrame 创建动画循环,在每帧中递增参数 t(如 t += 0.005),然后使用贝塞尔曲线公式计算物体在 t 位置的坐标,清除画布后重新绘制曲线和物体。
如何绘制等距的贝塞尔曲线点?
参数 t 等分不等于弧长等分。需要先细分 t 值(如 200 步),计算每个点的累积弧长,然后根据目标弧长在累积弧长数组中查找对应点。这种方法称为查表法或近似法。
二次和三次贝塞尔曲线如何选择?
简单弧线、圆角、平滑图表使用二次贝塞尔曲线,因为参数少、易于控制;S 形曲线、字体轮廓使用三次贝塞尔曲线,因为需要两个控制点来精确控制曲线形状。
本文涉及AI创作
内容由AI创作,请仔细甄别