連續bezier曲線的實現


需求場景

一系列的坐標點,划出一條平滑的曲線

3次Bezier曲線

基本上大部分繪圖工具都實現了3次Bezier曲線,4個點確定一條3次Bezier曲線。以html5中的canvas為例

let ctx = canvas.getContex('2d');
ctx.moveTo(20,20); // 曲線起點 Fom
ctx.bezierCurveTo(20,100,200,100,200,20); // 分別為控制點 Ctrl1,Ctrl2, 終點 To

連續Bezier曲線

假定給定點的序列List,我們應該以List中的每個點為起點,其下一個點Next為終點繪制Bezier曲線。
所以問題變成,如何確定這兩個點之間的兩個Bezier控制點。
每一小段路徑From-To的Bezier曲線並不是獨立的,其實收到了其前后兩個點的影響(Prev,Next)
我們在繪制每一段路徑的時候,引入其前點Prev,和后點Next共同計算當前Bezier曲線的控制點Ctrl1,Ctrl2
如圖所示
根據Prev,Next求當前控制點Ctrl1,Ctrl2

  1. 繪制從From到To的Bezier曲線,引入Prev,Next作參考點。
  2. 先依次連線4個點,記為線段l1,l2,l3,並求出其中點c1,c2,c3
  3. 連接中點,在c1c2上找一點f1, 使 l1:l2 = c1f1:f1c2。也就是 c1f1 = c1c2 * l1/(l1+l2)。我叫它線段比例法。看到有些算法是求其垂足,或者中點,嘗試之后發現這個線段比例點繪制的曲線最流暢
  4. 將線段 f1c2 平移到 起始點 From上,另一個端點就是所求的控制點Ctrl1。
  5. 通常我們會設置一個0-1的平滑度,乘以要平移的線段f1c2,然后得出最終的控制點。如果平滑度為0,那么其控制點就變成From本身,我們所畫出來的圖形就是折線。
  6. 同理求出終點To的控制點Ctrl2,計算過程注意平移位置關系。

我們留意到,當繪制第一段或者最后一段曲線時,沒有其前后參考點。這里我的做法是如果該點沒有Prev,Prev等於自身;如果沒有Next,令Next等於To。僅供參考

簡單代碼示例

Ps. 這段代碼並不能直接運行,僅僅是幫助理解,其中大部分點用向量表示,並省略了向量的實現細節。(如果只是想Ctrl+v的程序員,希望你從來沒看過這邊文章)

   
    /**
     * 獲取線段AB的k比例點,默認為1/2中點
     * @param {*} a 
     * @param {*} b 
     * @param {*} k 
     */
    getCenterPoint: function(a, b, k=0.5) {
        return a.add(b.sub(a).mul(k)); // 向量加減乘法,下同。a+(b-a)*k => 點a平移ab的k倍距離
    },

    /**
     * 獲取以c點為起點,以向量ab的平移的終點
     * @param {*} a 
     * @param {*} b 
     * @param {*} c 
     */
    getTransionPoint: function(a, b, c) {
        return c.add(b.sub(a).mul(this.smooth)); // 平移並乘以平滑度
    },

    /**
     * 計算Bezier控制點
     * @param {*} from 
     * @param {*} to 
     * @param {*} prev 
     * @param {*} next 
     */
    getBezierControlPoint: function(from, to, prev, next) {
        let p1 = this.getCenterPoint(prev, from);
        let p2 = this.getCenterPoint(from, to);
        let p3 = this.getCenterPoint(to, next);

        let f1,f2;
        // 使用垂足,不理想
        // f1 = this.getFootPoint(p1, p2, from, f1);
        // f2 = this.getFootPoint(p2, p3, to, f2);

        // 使用中點,不理想
        // f1 = this.getCenterPoint(p1, p2);
        // f2 = this.getCenterPoint(p2, p3);

        // 使用中點距離的比例點
        let len1 = prev.sub(from).mag(); // mag()計算向量prev-from的距離
        let len2 = from.sub(to).mag();
        let len3 = to.sub(next).mag();
        f1 = this.getCenterPoint(p1, p2, len1/(len1+len2)); // p1到p2的k倍距離點
        f2 = this.getCenterPoint(p2, p3, len2/(len2+len3));

        // 基於比例點作平移得到控制點 [Ctrl1, Ctrl2] 
        return [this.getTransionPoint(f1, p2, from), this.getTransionPoint(f2, p2, to)];
    },

判斷某一點是否在我們的連續Bezier曲線上

換句話說,判斷鼠標當前位置是否選中某一段Bezier曲線

  1. 如果序列點List的X有序(或者Y有序),(常見的例子是繪制圖表,X坐標軸是有序排列的),那么我們先依次對比所有的序列點X坐標,確定其唯一所在區間
  2. 否則,我們要對每一小段Bezier曲線進行判斷
  3. 判斷Bezier上的一點,我們需要理解Bezier曲線的原理和其函數。我們把這段曲線看作是一條路徑,假設從起點走到終點,需要花費10000個單位時間。對於每個單位之間t,我們可以用函數公式求得其坐標:

  1. 我們拿這10000個坐標點與我們的目標點比較,當其差值在一個可接受的范圍內時,我們認為目標點就在我們的Bezier曲線上。
  2. 應當注意的是,除了設置的誤差范圍外,分割的時間片(上面的10000)也會影響到最終結果。如果分割的時間片太少,導致間隙過大使得判斷失效。如果分割的時間片太多,又將嚴重提高計算所消耗的時間。
  3. 示例代碼
    /**
     * 判斷目標點P是否在Bezier曲線(p1,p2,p3,p4)上
     * @param {起點} p1 
     * @param {控制點1} p2 
     * @param {控制點2} p3 
     * @param {終點} p4 
     * @param {待判斷點} p 
     * @param {步長} step 
     * @param {誤差} range 
     * @return Bezier曲線的選中點
     */
    isBezierPoint: function(p1, p2, p3, p4, p, step=0.001, range=0.5) {
        for (let t = 0; t <= 1; t += step) {
            let x = p1.x*Math.pow(1-t, 3) + 3*p2.x*t*Math.pow(1-t, 2) + 3*p3.x*Math.pow(t, 2)*(1-t) + p4.x*Math.pow(t, 3);
            let y = p1.y*Math.pow(1-t, 3) + 3*p2.y*t*Math.pow(1-t, 2) + 3*p3.y*Math.pow(t, 2)*(1-t) + p4.y*Math.pow(t, 3);
            if (Math.abs(x - p.x) < range && Math.abs(y - p.y) < range) {
                return {x,y};
            }
        }
        return null;
    }

Bezier控制點的更新

特別注意一點:
假如我們的序列中某一點的位置發生改變,或者新增了一個序列點,那么其前后將有4個點(忽略端點)的控制點需要重新計算和更新,分別為:

  1. 以這個點為From的曲線(自身為起點)
  2. 以這個點為To的曲線(上一個點為起點)
  3. 以這個點為Prev的曲線(下一個點為起點)
  4. 以這個點為Next的曲線(上一個點的上一個點為起點)
for (let i=currentIndex-2, j=0; j<4; i++,j++) {
    // 更新控制點位置
    updateBezierControlPoint(i);
}

以上


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM