3阶(次)贝塞尔曲线的JavaScript(JS)实现

贝塞尔曲线简介:
贝塞尔曲线,是贝塞尔老爷子在使用电子计算机设计汽车零件的时候 进行曲面设计而采用的一种参数化的样条曲线.

一般参数方程:

\[B(t) = \sum_{i=0}^nC_n^iP_i(1-t)^{n-i}t^i \]

由公式很容易可以看出n阶贝塞尔曲线需要的点数是n+1个点,该公式为参数方程,并不是一般意义上的y=f(x),而是y = f(t),x = f(t).

贝塞尔曲线就是用来画曲线的,以三阶贝塞尔曲线为例,他有四个控制点,第一个点和最后一个点是这条曲线的起始点和终止点,曲线必定会经过这两个点,而第二个和第三个则是控制曲线形状的,更直接来说通过改变第二个点和第三个 点的位置,曲线的斜率就会受到影响。具体的影响可以直接打开chrome调试面板任意设置一个transition属性 然后观察其timing-function 看到效果。

上边说到了斜率,那其实在一个位移-时间的曲线方程中,斜率则代表了速度,实际在web动画中位移可以换成任何一个属性(详见早年间关于动画的一些论断)

那其实用js实现一条三阶贝塞尔曲线,无外呼是找一个 时间x -> 其他任意属性y 之间的映射。

这里,x我们是已知的,现在的需求就是解出y,以CSS的transition-timing-function做为一个参考,我们可以把起始点和终止点的坐标设置成(0,0)和(1,1) (实际很多东西都会这么处理,最后的结果做一个线性映射就好),自然两个控制点的范围也应该在0-1之间。

先将贝塞尔曲线展开成一般形式:

\[B(t) = P_0(1-t)^3 + 3P_1t(1-t)^2 + 3P2t^2(1-t) + P3t^3 \]

起始、终止点带入简化:

\[B(t) = 3P_1t(1-t)^2 + 3P_2t^2(1-t) + t^3 \]

OK,理论完成可以实践了。

假定,我们得到某一时刻的时刻值 x , 那么通过参数方程$$B(t) = x $$
可求得参数$$t$$的值,再将该$$t$$带入 $$y = B (t)$$,中即可求得我们想要的最终结果y。

所以,归根结底,第一件事情是要解方程,多次函数的求根并不容易,这里具体实现的时候,我们可以参考chromium的贝塞尔曲线实现,来解决这个问题,具体的做法是,首先通过8次牛顿迭代,如果找到了就直接return结果,如果没有,就开始Bisection_method(应该叫对分法)

牛顿迭代的原理,简而言之就是在一条曲线上任选一点做切线,然后在该切线与x轴的交点上做一条垂直于x轴的直线,假设该直线与曲线相交于另一个点,再在该点做切线。。。 一直重复此过程,切线于x轴的交点会越来越与曲线的根接近。

基本推导:
假设有曲线$$y = f(x)$$ 该曲线上任取一点$$x_0,y_0$$,做该点切线,
则,该点处切线的斜率为$$f^{(1)}(x_0)$$
由曲线方程$$ y = kx + b $$ 代入以上参数得

\[b = f(x_0) - f^{(1)}(x_0)x_0 \]

故 切线方程为 $$g(x) = f(x_0) - f^{(1)}(x_0)(x_0-x)$$
得到该切线与x轴得交点为$$x_1 = x_0 - \frac{f(x_0)}{f^{(1)}(x_0)}$$
这便是一次迭代。x1便是我们得到第一个近似根,在往后得迭代中,假如这个近似跟与实际根的误差在一个我们可接受的范围内,便可以将这个根当作真根。

Bisection_method的基本推导则是假如连续函数$$y = f(x)$$ 在区间$$[a,b]$$上连续,且$$f(a)$$与$$f(b)$$符号相反,那么函数$$y$$在区间$$[a,b]$$上至少有一个根。然后二分这个区间进行求值。

代码:


type coordinate = {
    x: number,
    y: number
}

export class cubicBezier{
    p1: coordinate
    p2: coordinate
    precision = 1e-5;
    constructor(x1,y1,x2,y2){
        this.p1 = {
            x:x1,
            y:y1
        };
        this.p2 = {
            x:x2,
            y:y2
        };
    }
    getX(t:number){
        let x1 = this.p1.x,x2=this.p2.x;
        return 3*x1*t*Math.pow(1-t,2) + 3* x2*Math.pow(t,2) * (1-t) + Math.pow(t,3)
    }
    getY(t:number){
        let y1 = this.p1.y,y2=this.p2.y;
        return 3*y1*t*Math.pow(1-t,2) + 3*y2*Math.pow(t,2) * (1-t) + Math.pow(t,3)
    }
    // https://github.com/amfe/amfe-cubicbezier/blob/master/src/index.js
    solveCurveX(x:number){
        var t2 = x;
        var derivative;
        var x2;

        var p1x = this.p1.x, p2x = this.p2.x;

        var ax = 3 * p1x - 3 * p2x + 1;
        var bx = 3 * p2x - 6 * p1x;;
        var cx = 3 * p1x;;

        function sampleCurveDerivativeX(t:number){
            // `ax t^3 + bx t^2 + cx t' expanded using Horner 's rule.
            return (3 * ax * t + 2 * bx) * t + cx;
        }
        // https://trac.webkit.org/browser/trunk/Source/WebCore/platform/animation
        // First try a few iterations of Newton's method -- normally very fast.
        // http://en.wikipedia.org/wiki/Newton's_method
        for (let i = 0; i < 8; i++) {
            // f(t)-x=0
            x2 = this.getX(t2) - x;
            if (Math.abs(x2) < this.precision) {
                return t2;
            }
            derivative = sampleCurveDerivativeX(t2);
            // == 0, failure
            if (Math.abs(derivative) < this.precision) {
                break;
            }
            // xn = x(n-1) - f(xn)/ f'(xn)
            // 假设g(x) = f(t) - x 
            // g'(x) = f'(t)
	    //所以  f'(t) == g'(t) 
            // derivative == g'(t)
            t2 -= x2 / derivative;
        }

        // Fall back to the bisection method for reliability.
        // bisection
        // http://en.wikipedia.org/wiki/Bisection_method
        var t1 = 1;
        var t0 = 0;

        t2 = x;
        while (t1 > t0) {
            x2 = this.getX(t2) - x;
            if (Math.abs(x2) < this.precision) {
                return t2;
            }
            if (x2 > 0) {
                t1 = t2;
            } else {
                t0 = t2;
            }
            t2 = (t1 + t0) / 2;
        }

        // Failure
        return t2;
    }
    solve(x:number){
        return this.getY( this.solveCurveX(x) )
    }
}
posted @ 2022-02-09 20:19  子龙_子龙  阅读(312)  评论(0编辑  收藏  举报