贝塞尔曲线解析

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。

贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由保尔·德·卡斯特里奥于1959年运用德卡斯特里奥算法开发,以稳定数值的方法求出贝塞尔曲线。

n阶贝塞尔曲线一般化推导公式如下:



swift 画线 贝塞尔曲线 js贝塞尔曲线动态画线_qgraphicsscene 鼠标画直线

是不是被公式的长度吓了一跳。不用担心,阶贝塞尔曲线其实并没有那么复杂,只要把原理理解明白了,任何人都可以轻松画出阶贝塞尔曲线。

下面我就带大家一起拆分各阶贝塞尔曲线,来揭开它的神秘面纱。

首先, 我们先来看下各阶的贝塞尔曲线动图

二阶的贝塞尔曲线就是一条直线, 就不再放图了, 图从三阶开始。



swift 画线 贝塞尔曲线 js贝塞尔曲线动态画线_qgraphicsscene 鼠标画直线_02


图1-三阶

swift 画线 贝塞尔曲线 js贝塞尔曲线动态画线_取得贝塞尔曲线x坐标的y值_03


图2-四阶

swift 画线 贝塞尔曲线 js贝塞尔曲线动态画线_canvas 画点_04


图3-五阶

从上面画各阶贝塞尔曲线的动图我们可以看出,各阶曲线是可以向下分解的。各阶曲线都可以递归的最终分解为三阶曲线。

在下图4中点 

 , 

 , 

 都处于自身所在线段的 

 处也就是在长度上 

 ,在 

 变动时, 

 , 

 和 

 的位置也在变动, 点

 的运行轨迹就是最终的贝塞尔曲线。

swift 画线 贝塞尔曲线 js贝塞尔曲线动态画线_css贝塞尔曲线 多个点_05


图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个点画图