贝塞尔曲线的javascript实现

前端入微2020-11-27 07:30:18

上一篇写了贝塞尔曲线的公式推导:浅谈贝塞尔曲线--公式推导,但是没有用代码实现。隔了一段时间,来填坑了。今天说下贝塞尔曲线的javascript实现。


贝塞尔曲线的通用公式为:

由公式可知,在给定时间t和控制点的时候,将t和控制点带入公式很容易得到曲线上的点。但是我们平时用贝塞尔曲线,主要是用曲线上x随y的变化趋势,即已知x求y。


所以代码主要分为两部分:

一,根据给定的x求对应的t;

二,将t带入公式得出y。


一、先说下简单的根据t求y

公式中有组合数,幂运算,还有多项求和。javascript的Math对象有幂运算方法pow()可以直接用,我们需要写一个求组合数的函数:

 1//求阶乘
2function factorial(n) {
3    let result = 1;
4    if (n < 0) {
5        console.log('阶乘必须非负')
6        return undefined;
7    } else if (n == 0) {
8        return 1;
9    } else {
10        for (let i = 1; i <= n; i++) {
11            result *= i;
12        };
13        return result;
14    }
15}
16
17//求组合数
18function combination(n, i) {
19    return factorial(n) / (factorial(i) * factorial(n - i));
20}


接下来就将多项式累加起来就好了。

 1//根据给定的t和控制点值,算出曲线上对应的x或y值
2function besizerPoint(t, groupPoint) {//给定控制点和时间,计算曲线
3    if (Object.prototype.toString.call(groupPoint) != '[object Array]') {//判断参数是否是数组
4        return false;
5    };
6    let n = groupPoint.length - 1;
7    let sum = 0;
8    for (let i = 0; i <= n; i++) {//将多项式的每一项累加
9        sum += combination(n, i)
10            * groupPoint[i]
11            * (Math.pow(t, i))
12            * (Math.pow((1 - t), (n - i)));
13    };
14    return sum;
15}


二、下面就是根据x求t

将x带入公式就是:

可以看出,这一部分的主要任务其实就是解一元高次方程。一元二次方程有特定公式,三次及三次以上就比较麻烦了。


有两种方法:二分法和牛顿迭代法。牛顿的东西真是从小学到大啊,所以我选择牛顿迭代法。


先放一段百度的牛顿迭代法介绍,

根据介绍,

我们只要用这个公式就好了。


还一个比较关键的就是如何选择开始迭代t,其实这个可以随便选,只是选的近,算的快,性能好。我个人猜测在坐标轴上,x与y的图形和x与t的图形,差不多的,所以我就把给定的x值当作初始迭代的t值。


好了,可以开始写这部分的实现代码了,

 1//牛顿迭代法
2//求曲线任意点的斜率
3function getSlope(guessT, point) {//point是控制点的x坐标
4    let n = point.length - 1;
5    let sum = 0;
6    for (let i = 0; i <= n; i++) {//导数的每一项累加
7        sum += combination(n, i)
8            * point[i]
9            * (n - i)
10            * (Math.pow((1 - guessT), (n - i - 1)))
11            * i
12            * (Math.pow(guessT, (i - 1)));
13    };
14    return sum;
15}
16
17//根据x值求对应的t值
18function besizerTime(pointX, groupPoint) {
19    let guessT, targetT;
20    guessT = pointX;//将目标x值,作为开始迭代的t值
21    targetT = guessT - (besizerPoint(guessT, groupPoint)-pointX) / getSlope(pointX, groupPoint);
22    while (Math.abs(targetT - guessT) > 0.001) {
23        guessT = targetT;
24        targetT = guessT - (besizerPoint(guessT, groupPoint)-pointX) / getSlope(pointX, groupPoint);
25    }
26    return targetT;
27}


至此,基本就差不多了,最后把两部分整合下,

 1//可以在下面函数添加定时器或屏幕刷新函数制作动画
2function besizer(groupPointX, groupPointY) {//控制点的坐标x与y,要按顺序分别放到两个数组里
3    let xValues = [0];//初始数组放个0,是因为前面函数没有写当值为0时,应该怎么处理,会返回NaN
4    let t;
5    let yValues = [0];//理由同上,目前不想再改这个bug,就这样应付下算了
6    for(let i = 0.1; i<=1;i+=0.1){
7        xValues.push(i);//生成横坐标x值
8        t = besizerTime(i, groupPointX);
9        yValues.push(besizerPoint(t, groupPointY));//生成纵坐标y值
10    };
11    console.log(yValues);
12    return yValues;//返回纵坐标数组
13}


这里,就算基本完成了,这个贝塞尔曲线理论上可以绘制三阶以上的曲线,但是没测试过