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

平面ABC 3個點。

在AB上找一個點D,在BC上找一個點E,使得AD:AB = BE:BC

然後在DE上找一個點F,使得DF:DE = AD:AB = BE:BC 接著,我們將D點從A點 --> B點慢慢移動,在這個過程中,會產生一系列的F點,將這些F點相連,就會形成一條曲線,嘿嘿,就是我們的貝塞爾曲線,

從這裡可以看出,這裡有3個關鍵點,起始點、終止點、控制點。 數學上的推理驗證,這裡就不講了,直接給出公式。
二階貝塞爾曲線,一個控制點


三階貝塞爾曲線,二個控制點


一階貝塞爾曲線,就是一條直線


為了完整性,我給出貝塞爾曲線的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條直接拼接而成,有了這個思路,已經完成了一半, 那條曲線該如何繪製,其實我覺得思路不止一種,我們應該先自己給這個曲線下定義,我認為他應該是半圓的弧連線,應該是橢圓的弧連結,應該是其他。我先給它下一個定義

我用二階和三階分別來描述這個曲線,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,請允許我盜圖

如何讓這個圖形動起來,可以這麼想第一個圓,可以是手開始觸控的點,也可以自己先寫死,另一個圓是手拖動的位置,所以只要動態的改變第二個圓心的位置,那麼這個拖動的效果就出來了。 我在拖動的時候,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連線的中點,具體的專案我晚一點附上地址。