大家好,我是前端西瓜哥。
今天我们开始学习贝塞尔曲线的算法。
我们有 p1(锚点 1)、cp1(控制点 1)、cp2(控制点 2)、p2(锚点 2) 表示的一条三阶贝塞尔曲线,给定曲线参数 t,求其对应的点位置,以及这个点的切向量和法向量。
求 t 对应的点
贝塞尔曲线本质是 线性插值 的升阶。
2 个 点组成直线(或者叫线性贝塞尔曲线),基于 t 进行线性插值,拿到插值点,这便是线性插值。
代码语言:javascript复制const lerp = (p1: Point, p2: Point, t: number) => {
return {
x: (1 - t) * p1.x t * p2.x,
y: (1 - t) * p1.y t * p2.y,
}
}
lerp 是 Linear interpolation(线性插值) 的缩写。
升阶为 3 个点(二阶贝塞尔曲线,p1、cp、p2),则这三个点依次连线,求出两个插值点,然后我们接着给这两个插值点的线性插值,得到 1 个带你。则这个点为该二阶贝塞尔曲线上 t 对应的点。
变成 4 个点(三阶贝塞尔曲线,p1、cp1、cp2、p2)也是同理,求出 3 个插值点,然后继续求出 2 个插值点,最后求出 1 个插值点。则这个点为该三阶阶贝塞尔曲线上 t 对应的点。
算法实现:
代码语言:javascript复制/** 计算三阶贝塞尔曲线 t 对应的点 */
const getBezier3Point = (
p1: Point,
cp1: Point,
cp2: Point,
p2: Point,
t: number,
) => {
const a = lerp(p1, cp1, t);
const b = lerp(cp1, cp2, t);
const c = lerp(cp2, p2, t);
const e = lerp(a, b, t);
const f = lerp(b, c, t);
return lerp(e, f, t);
};
上面这个算法还可以优化,将其化简成一个专用的三阶贝塞尔曲线公式,以减少运算量,读者可自行尝试。
算法对应的示意图:
如果变成 N 个点,也一样,计算 N-1 个插值点,然后是 N-2,最后变成只有 1 个的时候,就是这个 N 阶贝塞尔曲线 t 对应的点。
我们可以实现一个通用的方法:
代码语言:javascript复制/** 计算 N 阶贝塞尔曲线 t 对应的点 */
const getBezierNPoint = (pts: Point[], t: number) => {
while (pts.length > 1) {
const nextPts = [];
for (let i = 0, size = pts.length - 1; i < size; i ) {
nextPts.push(lerp(pts[i], pts[i 1], t));
}
pts = nextPts;
}
return pts[0];
};
求切向量
接着我们来求三阶贝塞尔曲线 t 所在点的切向量(tangent vector)。
切向量是描述曲线上某一点相切的向量。
上面我们知道,通过对多个连续点不断做线性插值,减少到插值点只有 1 个为止,此时这个点就是 t 对应的点。
那如果我们 让插值点保留位为 2 个,就能得到一条线,这条线便是 t 对应点的 切线。
代码语言:javascript复制/** 计算三阶贝塞尔曲线 t 位置的切线 */
const getBezier3TangentLine = (
p1: Point,
cp1: Point,
cp2: Point,
p2: Point,
t: number,
) => {
const a = lerp(p1, cp1, t);
const b = lerp(cp1, cp2, t);
const c = lerp(cp2, p2, t);
return [lerp(a, b, t), lerp(b, c, t)];
};
切线求出来了,切向量自然也能计算出来了。这里使用贝塞尔前进方向为切向量方向。
代码语言:javascript复制/** 计算三阶贝塞尔曲线 t 位置的切向量 */
const getBezier3Tangent = (
p1: Point,
p2: Point,
cp1: Point,
cp2: Point,
t: number,
) => {
const [a, b] = getBezier3TangentLine(p1, p2, cp1, cp2, t);
const dist = distance(a, b);
return {
x: (b.x - a.x) / dist,
y: (b.y - a.y) / dist,
};
};
求法向量
法向量是垂直于点所在切线的向量。
也就是是切向量旋转 90 度。
法向量也有两个方向,这里我们选择贝塞尔前进方向的右方作为法向量方向。
代码语言:javascript复制/** 计算三阶贝塞尔曲线 t 位置的法向量 */
const getBezier3Normal = (
p1: Point,
p2: Point,
cp1: Point,
cp2: Point,
t: number,
) => {
const tangent = getBezier3Tangent(p1, p2, cp1, cp2, t);
return {
x: -tangent.y,
y: tangent.x,
}
};
演示
结尾
我是前端西瓜哥,关注我,学习更多平面几何知识。