大家好,我是前端西瓜哥。
图形编辑器中,我们有时候希望在 path 中微调一条曲线,在中间新增一个锚点,然后调整这个锚点和它的控制点。
为了实现这个功能,我们需要实现算法:将一条贝塞尔曲线拆分为两条贝塞尔曲线。
下面我们将以三阶贝塞尔为例进行说明,其他阶贝塞尔曲线是同理的。
我们需要用到 De Casteljau 算法,一般我们计算贝塞尔上 t 对应的点,会直接带入参数方程计算,De Casteljau 算法则是遵循古法,从贝塞尔的定义入手。
即将点依次相连为线段,然后进行 线性插值 取线段 t 位置上的点,然后继续同样的操作,这样不断递归线性插值直到只剩下一个点,这个点就是 t 在贝塞尔曲线上对应的点。
交互演示
原理
假设我们想在 t1 的位置,将三阶贝塞尔拆成左右两部分。
看图可知,对于左边,其实就是将原来 t 从 0 到 1 才走完所有的插值,变成 0 到 t1 就算走完,这样我们就能产生一个局部的三阶贝塞尔曲线。
上面这几个点就是拆分后左侧三阶贝塞尔的 4 个点,它们是线性插值递归过程中产生的。
右侧同理,为 t1 到 1 的这部分插值形成的曲线。
算法实现
所以算法实现就是:
const splitCubicBezier = (p1, p2, p3, p4, t) => {
// 第一次线性插值
const a = lerp(p1, p2, t);
const b = lerp(p2, p3, t);
const c = lerp(p3, p4, t);
// 第二次线性插值
const d = lerp(a, b, t);
const e = lerp(b, c, t);
// 第三次线性插值
const f = lerp(d, e, t);
return [
[p1, a, d, f],
[f, e, c, p4],
];
};
// 线性插值
const lerp = (p1: Point, p2: Point, t: number): Point => {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t,
};
};
可视化交互
写个交互验证一下。
推广到任意阶贝塞尔
然后我们就发现了其中的规律,可以推广到任意阶的贝塞尔曲线。
我们将每轮线性插值产生的点记录下来,每一轮递归都会产生一批点,每一轮数量都会少一个,最后会形成一个倒三角的形状。
[p0, p1, p2, p3]
[p01, p12, p23]
[p012, p123]
[p01234]
则左边为:
[p0, p01, p012, p01234]
右边为:
[p01234, p123, p23, p3]
左侧贝塞尔就是每个数组的第一个元素组成的顺序数组。
右侧贝塞尔就是每个数组末尾元素组成的倒序数组。
算法实现:
const splitBezier = (points, t) => {
const degree = points.length - 1; // 贝塞尔阶数
const layers: Point[][] = [[...points]]; // 存储每一层的插值点
// 计算 de Casteljau 三角形
for (let i = 1; i <= degree; i++) {
const currentLayer: Point[] = [];
const prevLayer = layers[i - 1];
// 计算当前层的插值点
for (let j = 0; j < degree - i + 1; j++) {
currentLayer.push(lerp(prevLayer[j], prevLayer[j + 1], t));
}
layers.push(currentLayer);
}
// 取第一个元素,顺序
const leftPoints: Point[] = [];
for (let i = 0; i <= degree; i++) {
leftPoints.push(layers[i][0]);
}
// 取末尾数组,倒序
const rightPoints: Point[] = [];
for (let i = degree; i >= 0; i--) {
rightPoints.push(layers[i][layers[i].length - 1]);
}
return [leftPoints, rightPoints];
};
结尾
我是前端西瓜哥,关注我,学习更多平面几何知识。
相关阅读,