贝塞尔曲线解析
贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。
贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由保尔·德·卡斯特里奥于1959年运用德卡斯特里奥算法开发,以稳定数值的方法求出贝塞尔曲线。
n阶贝塞尔曲线一般化推导公式如下:
是不是被公式的长度吓了一跳。不用担心,阶贝塞尔曲线其实并没有那么复杂,只要把原理理解明白了,任何人都可以轻松画出阶贝塞尔曲线。
下面我就带大家一起拆分各阶贝塞尔曲线,来揭开它的神秘面纱。
首先, 我们先来看下各阶的贝塞尔曲线动图
二阶的贝塞尔曲线就是一条直线, 就不再放图了, 图从三阶开始。
图1-三阶
图2-四阶
图3-五阶
从上面画各阶贝塞尔曲线的动图我们可以看出,各阶曲线是可以向下分解的。各阶曲线都可以递归的最终分解为三阶曲线。
在下图4中点
,
,
都处于自身所在线段的
处也就是在长度上
,在
变动时,
,
和
的位置也在变动, 点
的运行轨迹就是最终的贝塞尔曲线。
图4
四阶贝塞尔曲线就相当于在三阶的基础上在外面再包一层线,外层三条线的每条线的
点依次连线形成和三阶一样的结构,然后在使用和三阶一样的方法可得到线段
,从而得到点
,最终得到贝塞尔曲线。更高阶的贝塞尔曲线原理也和四阶一样,每条线的
点依次连线形成低阶结构最终得到动点
,从而得到最终的贝塞尔曲线。
上代码
根据给出的点递归计算低阶曲线的对应的点,最终计算出不系列动点
。
function drawLines(ps, color) {
if (ps.length < 2) {
const { x, y } = ps[0];
finalPoints.push({ x, y });
drawLine();
return;
}
const pps = [];
for (let i = 0; i < ps.length - 1; i++) {
pps.push(calcMiddlePoint(ps[i], ps[i + 1], .5));
}
drawLines(pps);
}
计算两点连线t点坐标
// 计算两点连线t点坐标function calcMiddlePoint(p1, p2) {
return { x: (p2.x - p1.x) * t + p1.x, y: (p2.y - p1.y) * t + p1.y };
}
画最终曲线
function drawLine() {
let last = {}
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
ctx.beginPath();
finalPoints.forEach(({ x, y }, index) => {
if (index === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
});
ctx.stroke();
}
完整代码
<html lang="zh-hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贝塞尔曲线title>
<style>html,body,
#canvas {margin: 0;padding: 0;background: white;
}html,body {height: 100%;width: 100%;background: gray;
}
.buttonContainer {position: fixed;right: 20px;bottom: 20px;z-index: 100;
}style>head>
<body>
<canvas id="canvas">canvas>
<div class="buttonContainer">
<div>
<button id="draw">画图button>
<button id="reDraw">重画button>
<button id="clear">清空button>
<button id="radom">随机button>div>
<div>
<a href="https://hefang.link">何方博客a>
<a href="https://zhuanlan.zhihu.com/iamhefang">何方的知乎专栏a>div>div>
<script>const cv = document.getElementById("canvas");const draw = document.getElementById("draw");const clear = document.getElementById("clear");const reDraw = document.getElementById("reDraw")let points = [];const ctx = cv.getContext("2d");let offset = 0;let timer;
cv.height = window.innerHeight;
cv.width = window.innerWidth;function calcMiddlePoint(p1, p2) {return { x: (p2.x - p1.x) * offset + p1.x, y: (p2.y - p1.y) * offset + p1.y };
}let finalPoints = [];function drawLine() {let last = {}
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
ctx.beginPath();
finalPoints.forEach(({ x, y }, index) => {if (index === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
});
ctx.stroke();
}function drawLines(ps, color) {if (ps.length < 2) {const { x, y } = ps[0];
finalPoints.push({ x, y });
drawLine();return;
}
ctx.lineWidth = 1;
ctx.strokeStyle = color || "green";
ctx.beginPath();for (let i = 0; i < ps.length; i++) {const { x, y } = ps[i];if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();const pps = [];for (let i = 0; i < ps.length - 1; i++) {
pps.push(calcMiddlePoint(ps[i], ps[i + 1], .5));
}
drawLines(pps)
}function doDraw() {if (points.length < 2) {
alert(`无法画图,请至少指定2个点`);return;
}
timer && clearInterval(timer);
drawLines(points, 'blue');
timer = setInterval(() => {
offset += 0.001;window.requestAnimationFrame(() => {
ctx.clearRect(0, 0, cv.width, cv.height);
drawLines(points, 'blue');
});if (offset >= 1) {
clearInterval(timer);
}
}, 1);
}
reDraw.addEventListener("click", function () {
finalPoints = [];
offset = 0;
ctx.clearRect(0, 0, cv.width, cv.height);
doDraw();
});
draw.addEventListener("click", doDraw);
clear.addEventListener("click", () => {
points = [];
finalPoints = [];
offset = 0;
ctx.clearRect(0, 0, cv.width, cv.height);
});
cv.addEventListener("click", function (e) {
points.push({ x: e.clientX, y: e.clientY });
console.log("point:", { x: e.clientX, y: e.clientY });
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(e.clientX, e.clientY, 2, 0, Math.PI * 2);
ctx.fill();
});
radom.addEventListener("click", () => {
points = [];
finalPoints = [];
offset = 0;
ctx.clearRect(0, 0, cv.width, cv.height);for (let i = 0; i < 10; i++) {
points.push({ x: Math.random() * cv.width, y: Math.random() * cv.height })
}
doDraw();
});script>body>html>
上面的html我已经放到了服务器上
- 鼠标直接屏幕点击画点
- 点击右下角"画图"开始画图
- 点击"重画"可再次画图
- 点击"清空"清空结果
- 点击”随机“使用随机生成的20个点画图