贝塞尔曲线理解与应用

贝塞尔曲线并非是由贝塞尔发明的,但是是因为他把这个东西应用到当时的汽车领域而闻名的,所以取名为贝塞尔曲线。
在我看来,用简单的话来理解一下贝塞尔曲线,他是通过少量几个点,使用一套公式,生成一条平滑曲线。

原理

先盗用人家的图,嘿嘿。

Alt text
平面ABC 3个点。
Alt text
在AB上找一个点D,在BC上找一个点E,使得AD:AB = BE:BC
Alt text
然后在DE上找一个点F,使得DF:DE = AD:AB = BE:BC 接着,我们将D点从A点 --> B点慢慢移动,在这个过程中,会产生一系列的F点,将这些F点相连,就会形成一条曲线,嘿嘿,就是我们的贝塞尔曲线,
Alt text
从这里可以看出,这里有3个关键点,起始点、终止点、控制点。 数学上的推理验证,这里就不讲了,直接给出公式。

二阶贝塞尔曲线,一个控制点

Alt text
Alt text

三阶贝塞尔曲线,二个控制点

Alt text
Alt text

一阶贝塞尔曲线,就是一条直线

Alt text
Alt text

为了完整性,我给出贝塞尔曲线的n阶通式

Alt text
想看这个公式推导,我给出一个文章链接 n公式推导推导。 但是在一般应用中,二阶,三阶贝塞尔曲线是已经够用了。

应用

先简单的来使用一下,通过公式来描绘曲线。

***

        d2(){
            this.name = '二次贝赛尔曲线方程';
            let _this = this;
            let oCanvas = document.querySelector("#canvas"),
            oGc = oCanvas.getContext('2d');
            let percent = 0;
            function animate() {
                oGc.clearRect(0, 0, 800, 800);
                oGc.beginPath();
                oGc.strokeStyle = 'red';
                oGc.moveTo( 40, 80 );
                //oGc.quadraticCurveTo( 137, 80, 140, 280 );
                _this.d2_(oGc,[40, 80],[137, 80],[140, 280],percent);
                oGc.stroke();
                percent = (percent + 1) % 100;
                requestAnimationFrame(animate);
            }
            animate()
        },
        d2_(oGc,start,cp,end, percent){
            for (var t = 0; t <=  percent / 100; t += 0.01) { 
                var x = this.quadraticBezier(start[0], cp[0], end[0], t); 
                var y = this.quadraticBezier(start[1], cp[1], end[1], t);
                oGc.lineTo(x, y);
            } 
        },
        quadraticBezier(p0, p1, p2, t) {
            var k = 1 - t;
            return k * k * p0 + 2 * (1 - t) * t * p1 + t * t * p2; // 这个方程就是二次贝赛尔曲线方程 
        },

***
复制代码

这个就是根据公式描述出相关的点,然后连接起来。 但是在实际应用中,很大程度上会在canvas中绘图,canvas提供2个api,
quadraticCurveTo:二阶贝塞尔曲线,参数是 控制点,结束点
bezierCurveTo :三阶贝塞尔曲线,参数是 控制点1,控制点2,结束点
你们发现没,它们没有开始点,它们的开始点是画笔开始的位置。

在举一个例子,画起伏波浪

直接讲思路,就是先画一个静止的波浪

好,现在来看一下,这个该怎么入手,先把这个轮廓描绘出来,要描绘,先拆分, 它是由一条曲线,3条直接拼接而成,有了这个思路,已经完成了一半, 那条曲线该如何绘制,其实我觉得思路不止一种,我们应该先自己给这个曲线下定义,我认为他应该是半圆的弧连接,应该是椭圆的弧链接,应该是其他。我先给它下一个定义

Alt text

我用二阶和三阶分别来描述这个曲线,1,2,3,4这4个点描述出来了,那么这个曲线也就绘制完成了
1: (0.5d,waveH)
2: (d, 0)
3: (1.5d,-waveH)
4: (2d,0)
我选择的这个规则是很中规中矩的,上一个波形是画2个二阶贝塞尔曲线,下一个波形是画一个3阶贝塞尔曲线。这个就可以把静止的波形给绘制出来了,然后你想象一个给这个坐标加横向偏移,加纵向偏移,他就可以起伏波动了

***
            init2(){
                this.name = '2阶';
                let c = document.getElementById("myCanvas"),
                    ctx = c.getContext("2d"),

                    waveWidth = 800,
                    offset = 0, //x
                    waveHeight = 20, // 波浪大小
                    waveCount = 5,
                    startX = -200,
                    startY = 208,
                    progress = 0,  //高度
                    progressStep = 0.5,
                    d2 = waveWidth / waveCount,
                    d = d2 / 2,
                    hd = d / 2;
                    
                    ctx.fillStyle = "rgba(0,222,255, 0.2)";
                    function tick() {
                        offset -= 4;  // x 移动
                        
                        progress += progressStep;
                        if (progress > 220 || progress < 0) progressStep *= -1;
                     
                        if (-1 * offset === d2) offset = 0;
                        
                        ctx.clearRect(0, 0, c.width, c.height);
                        ctx.beginPath();
                        
                        let offsetY = startY - progress; //y 坐标高低
                        ctx.moveTo(startX - offset, offsetY);
                     
                        for (var i = 0; i < waveCount; i++) {
                            var dx = i * d2;
                            var offsetX = dx + startX - offset;
                            ctx.quadraticCurveTo(offsetX + hd, offsetY + waveHeight, offsetX + d, offsetY);
                            ctx.quadraticCurveTo(offsetX + hd + d, offsetY - waveHeight, offsetX + d2, offsetY);
                        }
                        ctx.lineTo(startX + waveWidth, 300);
                        ctx.lineTo(startX, 300);
                        ctx.fill();
                     
                        requestAnimationFrame(tick);
                    }
                     
                    tick();

            },
***
复制代码

上面是二阶贝塞尔曲线,用三阶画的话,就是
ctx.quadraticCurveTo(offsetX + hd, offsetY + waveHeight, offsetX + d, offsetY);
ctx.quadraticCurveTo(offsetX + hd + d, offsetY - waveHeight, offsetX + d2, offsetY);

换成
ctx.bezierCurveTo(offsetX + hd, offsetY + waveHeight, offsetX + d + hd, offsetY-waveHeight, offsetX + d2, offsetY );
就可以了。

其实我觉得贝塞尔曲线在使用过程中,最关键的是控制点的选择,不同点的选择,会展现不同的效果,但是选择控制点,是一件挺有意思的事。
下面我们再来看一个案例,粘性拖动

Alt text
要实现这个功能,来理一下思路,首先来描绘一下这个轮廓,一样的套路,是不是,来,思考一下,这个图形是由什么组成的。
画的丑,别介意,这么看这个轮廓,是不是出来了,你可以想象,是由2个半圆的圆弧和2条曲线,可以先画ABCD这个路径,再画2个圆,这样这个轮廓就出来了。接下来,再看这个曲线如何完成。这个曲线开始和结束点已经有了,再找一个控制点也能画出来,那么控制点在哪里,我下的定义简单粗暴,在2圆心的链接线的终点,然后再把ABCD 4个点描述出来,这个路径就解决了,如何描述ABCD,请允许我盗图
Alt text
如何让这个图形动起来,可以这么想第一个圆,可以是手开始触摸的点,也可以自己先写死,另一个圆是手拖动的位置,所以只要动态的改变第二个圆心的位置,那么这个拖动的效果就出来了。 我在拖动的时候,d的距离在改变,那么制定一个规则,d越大,第一个圆的半径就越小,那么基本上就可以实现了。

***
    data() {
        return {
            radius: 7,
            x: 300,//手移动
            y: 300,//手移动
            anchorX: 200,// 控制点
            anchorY: 200,// 控制点
            startX: 100, //开始
            startY: 100,//开始
        }
    },
    mounted() {
        document.removeEventListener('touchstart', this.wrapTouchStart);
        document.addEventListener("touchstart", this.wrapTouchStart);

        document.removeEventListener('touchmove', this.wrapTouchMove);
        document.addEventListener('touchmove', this.wrapTouchMove);

        document.removeEventListener('touchend', this.wrapTouchEnd);
        document.addEventListener('touchend', this.wrapTouchEnd);

        document.removeEventListener('touchcancel', this.wrapTouchCancel);
        document.addEventListener('touchcancel', this.wrapTouchCancel);
    },
    methods: {
        wrapTouchStart(e) {},
        wrapTouchMove(e) {
            this.x = e.changedTouches[0].clientX;
            this.y = e.changedTouches[0].clientY;
            this.anchorX = (e.changedTouches[0].clientX + this.startX) / 2;
            this.anchorY = (e.changedTouches[0].clientY + this.startY) / 2;
            this.d2();
        },
        wrapTouchEnd() {
            this.radius = 20;

            // 手势坐标
            this.x = 300;
            this.y = 300;

            // 控制点坐标
            this.anchorX = 200;
            this.anchorY = 200;

            // 起点坐标
            this.startX = 100;
            this.startY = 100;
        },
        wrapTouchCancel() {
            let oCanvas = document.querySelector("#canvas"),
                ctx = oCanvas.getContext('2d');
            ctx.clearRect(0, 0, 360, 600);
        },
        d2() {

            let _this = this;
            let oCanvas = document.querySelector("#canvas");
            ctx = oCanvas.getContext('2d');
            ctx.strokeStyle = 'red';


            var distance = Math.sqrt(Math.pow(this.y - this.startY, 2) + Math.pow(this.x - this.startX, 2));
            this.radius = -distance / 15 + 20;
            
            // 当气泡拉到一定程度,断开链条且链条消失
            //if (this.radius < 7) {
            if(distance > 250){
                ctx.clearRect(0, 0, 360, 600);
                ctx.beginPath();
                ctx.arc(this.x, this.y, 20, 0, 2 * Math.PI);
                ctx.strokeStyle = 'red';
                ctx.fill();

                console.log('end');
                return;
            }

            let sin = (this.x - this.startX) / distance;
            let cos = (this.y - this.startY) / distance;


            var x1 = this.startX - this.radius * cos;
            var y1 = this.startY + this.radius * sin;

            var x2 = this.x - 20 * cos;
            var y2 = this.y + 20 * sin;

            var x3 = this.x + 20 * cos;
            var y3 = this.y - 20 * sin;

            var x4 = this.startX + this.radius * cos;
            var y4 = this.startY - this.radius * sin;

            ctx.clearRect(0, 0, 360, 600);
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.quadraticCurveTo(this.anchorX, this.anchorY, x2, y2);
            ctx.lineTo(x3, y3);
            ctx.quadraticCurveTo(this.anchorX, this.anchorY, x4, y4);
            ctx.lineTo(x1, y1);
            ctx.fillStyle = 'red'; 
            ctx.stroke();
            ctx.fill();

            // 两圆圈
            ctx.beginPath();
            ctx.arc(this.startX, this.startY, this.radius, 0, 2 * Math.PI)
            ctx.arc(this.x, this.y, 20, 0, 2 * Math.PI)
            ctx.strokeStyle = 'red';

            ctx.fill();

        },
    }
    ***
复制代码

到这里,应该要结束了,但是我想说这控制点,其实还有其他选择,还有一种是是AC连线的中点,和BD连线的中点,具体的项目我晚一点附上地址。

猜你喜欢

转载自juejin.im/post/5bc7ebf0f265da0ac07c8456