貝塞爾曲線理解與應用

zw_at_nicomama發表於2018-10-19

貝塞爾曲線並非是由貝塞爾發明的,但是是因為他把這個東西應用到當時的汽車領域而聞名的,所以取名為貝塞爾曲線。
在我看來,用簡單的話來理解一下貝塞爾曲線,他是通過少量幾個點,使用一套公式,生成一條平滑曲線。

原理

先盜用人家的圖,嘿嘿。

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 );
就可以了。

其實我覺得貝塞爾曲線在使用過程中,最關鍵的是控制點的選擇,不同點的選擇,會展現不同的效果,但是選擇控制點,是一件挺有意思的事。
下面我們再來看一個案例,粘性拖動

貝塞爾曲線理解與應用
要實現這個功能,來理一下思路,首先來描繪一下這個輪廓,一樣的套路,是不是,來,思考一下,這個圖形是由什麼組成的。
貝塞爾曲線理解與應用
畫的醜,別介意,這麼看這個輪廓,是不是出來了,你可以想象,是由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連線的中點,具體的專案 github.com/chengchengc…

by cs

相關文章