簡體   English   中英

如何計算避免物體的貝塞爾曲線控制點?

[英]How to calculate bezier curve control points that avoid objects?

具體來說,我正在使用javascript在canvas中工作。

基本上,我的對象有我想要避免的邊界,但仍然圍繞着貝塞爾曲線。 但是,我甚至不確定從哪里開始編寫一個可以移動控制點以避免碰撞的算法。

問題出現在下圖中,即使您不熟悉音樂符號,問題仍然應該是相當清楚的。 曲線的點是紅點

此外,我可以訪問每個音符的邊界框,其中包括詞干。

在此輸入圖像描述

所以很自然地,必須在邊界框和曲線之間檢測到碰撞(這里的某個方向會很好,但我一直在瀏覽並看到有相當數量的信息)。 但是在檢測到碰撞后會發生什么? 計算控制點位置以制作看起來更像的東西會發生什么:

在此輸入圖像描述

貝齊爾的方法

最初的問題是一個廣泛的問題 - 甚至可能更廣泛的問題,因為有許多不同的情景需要考慮制定“一個適合所有人的解決方案”。 這是一個完整的項目。 因此,我將為您提供一個可以構建的解決方案的基礎 - 它不是一個完整的解決方案(但接近一個......)。 我在最后添加了一些關於添加的建議。

此解決方案的基本步驟是:

將筆記分為兩組,左側和右側。

然后,控制點基於從第一個(結束)點到該組中任何其他音符的​​最大角度,以及到第二個組中任何點的最后一個結束點。

然后將來自兩組的所得角度加倍(最大90°)並用作計算控制點(基本上是點旋轉)的基礎。 可以使用張力值進一步修剪距離。

角度,倍增,距離,張力和填充偏移將允許微調以獲得最佳的總體結果。 可能存在需要額外條件檢查的特殊情況,但這不在此范圍內(它不是完整的密鑰就緒解決方案,但為進一步工作提供了良好的基礎)。

來自該流程的幾個快照:

圖像2

圖像3

示例中的主要代碼分為兩部分,兩部分解析每一半以找到最大角度和距離。 這可以合並為一個循環,並且除了從左到中之外還有從右到中的第二個迭代器,但為了簡單起見並更好地理解發生了什么,我將它們分成兩個循環(並引入了一個bug在下半場 - 請注意。我會把它留作練習):

var dist1 = 0,  // final distance and angles for the control points
    dist2 = 0,
    a1 = 0,
    a2 = 0;

// get min angle from the half first points
for(i = 2; i < len * 0.5 - 2; i += 2) {

    var dx = notes[i  ] - notes[0],      // diff between end point and
        dy = notes[i+1] - notes[1],      // current point.
        dist = Math.sqrt(dx*dx + dy*dy), // get distance
        a = Math.atan2(dy, dx);          // get angle

    if (a < a1) {                        // if less (neg) then update finals
        a1 = a;
        dist1 = dist;
    }
}

if (a1 < -0.5 * Math.PI) a1 = -0.5 * Math.PI;      // limit to 90 deg.

與下半部分相同,但在這里我們翻轉角度,以便通過比較當前點與終點而不是與當前點相比的終點來更容易處理。 循環完成后,我們將其翻轉180°:

// get min angle from the half last points
for(i = len * 0.5; i < len - 2; i += 2) {

    var dx = notes[len-2] - notes[i],
        dy = notes[len-1] - notes[i+1],
        dist = Math.sqrt(dx*dx + dy*dy),
        a = Math.atan2(dy, dx);

    if (a > a2) {
        a2 = a;
        if (dist2 < dist) dist2 = dist;            //bug here*
    }
}

a2 -= Math.PI;                                     // flip 180 deg.
if (a2 > -0.5 * Math.PI) a2 = -0.5 * Math.PI;      // limit to 90 deg.

(錯誤是即使較短的距離點具有更大的角度,也會使用最長的距離 - 我現在將其視為一個例子。它可以通過反轉迭代來修復。)。

我發現的關系很好,是地板和點之間的角度差兩倍:

var da1 = Math.abs(a1);                            // get angle diff
var da2 = a2 < 0 ? Math.PI + a2 : Math.abs(a2);

a1 -= da1*2;                                       // double the diff
a2 += da2*2;

現在我們可以簡單地計算控制點並使用張力值來微調結果:

var t = 0.8,                                       // tension
    cp1x = notes[0]     + dist1 * t * Math.cos(a1),
    cp1y = notes[1]     + dist1 * t * Math.sin(a1),
    cp2x = notes[len-2] + dist2 * t * Math.cos(a2),
    cp2y = notes[len-1] + dist2 * t * Math.sin(a2);

瞧:

ctx.moveTo(notes[0], notes[1]);
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, notes[len-2], notes[len-1]);
ctx.stroke();

增加逐漸減少的效果

要創建曲線,可以通過以下操作簡單地添加逐漸變細的漸變:

在添加第一條貝塞爾曲線之后,不是撫摸路徑,而是以微小的角度偏移調整控制點。 然后通過添加另一條從右到左的Bezier曲線繼續路徑,最后填充它( fill()將關閉隱含的路徑):

// first path from left to right
ctx.beginPath();
ctx.moveTo(notes[0], notes[1]);                    // start point
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, notes[len-2], notes[len-1]);

// taper going from right to left
var taper = 0.15;                                  // angle offset
cp1x = notes[0] + dist1*t*Math.cos(a1-taper);
cp1y = notes[1] + dist1*t*Math.sin(a1-taper);
cp2x = notes[len-2] + dist2*t*Math.cos(a2+taper);
cp2y = notes[len-1] + dist2*t*Math.sin(a2+taper);

// note the order of the control points
ctx.bezierCurveTo(cp2x, cp2y, cp1x, cp1y, notes[0], notes[1]);
ctx.fill();                                        // close and fill

最終結果(偽筆記 - 張力= 0.7,填充= 10)

錐體

小提琴

建議的改進:

  • 如果兩組的距離都很大,或者角度很陡,它們可能會被用作減少張力(距離)或增加它(角度)的總和。
  • 優勢/面積因素可能會影響距離。 優勢表示最高的部件在哪里移位(它是否位於左側或右側,並相應地影響每側的張力)。 這可能/可能足夠自己但需要進行測試。
  • 錐角偏移也應與距離之和有關。 在某些情況下,線條交叉並且看起來不那么好。 可以用解析Bezier點(手動實現)的手動方法替換錐度,並根據陣列位置在原始點和返回路徑的點之間添加距離。

希望這可以幫助!

紅衣主教樣條和過濾方法

如果您打開使用非Bezier方法,則以下可以在音符桿上方給出近似曲線。

該解決方案包括4個步驟:

  1. 收集音符/莖的頂部
  2. 過濾掉路徑中的“逢低”
  3. 過濾掉同一斜坡上的點
  4. 生成基數樣條曲線

這是一個原型解決方案,所以我沒有針對所有可能的組合進行測試。 但它應該給你一個良好的起點和基礎繼續。

第一步很簡單,收集代表音符桿頂部的點 - 對於演示,我使用以下點集合,稍微代表你在帖子中的圖像。 它們按x,y順序排列:

var notes = [60,40, 100,35, 140,30, 180,25, 220,45, 260,25, 300,25, 340,45];

這將表示如下:

此搜索

然后我創建了一個簡單的多遍算法,可以濾除相同斜率上的傾角和點。 算法中的步驟如下:

  • 雖然有另一個anotherPass (真)它將繼續,或直到最初設置的最大通過次數
  • 只要未設置skip標志,該點就會復制到另一個數組
  • 然后它會將當前點與下一個點進行比較,看它是否有下坡
  • 如果是,它會將下一個點與下面的點進行比較,看它是否有上升斜率
  • 如果是,它被認為是傾角並且設置了skip標志,因此不會復制下一個點(當前中間點)
  • 下一個濾波器將比較當前和下一個點之間的斜率,以及下一個點和下一個點。
  • 如果它們是相同的skip標志被設置。
  • 如果必須設置skip標志,它還將設置anotherPass標志。
  • 如果沒有過濾的點(或達到最大通過量),循環將結束

核心功能如下:

while(anotherPass && max) {

    skip = anotherPass = false;

    for(i = 0; i < notes.length - 2; i += 2) {

        if (!skip) curve.push(notes[i], notes[i+1]);
        skip = false;

        // if this to next points goes downward
        // AND the next and the following up we have a dip
        if (notes[i+3] >= notes[i+1] && notes[i+5] <= notes[i+3]) {
            skip = anotherPass = true;
        }

        // if slope from this to next point = 
        // slope from next and following skip
        else if (notes[i+2] - notes[i] === notes[i+4] - notes[i+2] &&
            notes[i+3] - notes[i+1] === notes[i+5] - notes[i+3]) {
            skip = anotherPass = true;
        }
    }
    curve.push(notes[notes.length-2], notes[notes.length-1]);
    max--;

    if (anotherPass && max) {
        notes = curve;
        curve = [];
    }
}

第一次傳遞的結果是在偏移y軸上的所有點之后 - 注意忽略傾斜音符:

圖像2

在完成所有必要的傳遞之后,最終的點數組將表示為:

圖像3

剩下的唯一步驟是使曲線平滑。 為此,我使用了我自己的基數樣條實現(在MIT下許可,可以在這里找到 ),它采用x,y點和平滑的數組,它根據張力值添加插值點。

它不會產生完美的曲線,但結果將是:

image6

小提琴

有一些方法可以改善我沒有解決過的視覺效果,但如果您覺得有必要,我會留給您做。 其中可能是:

  • 找到點的中心並根據角度增加偏移量,使其在頂部更加圓弧
  • 平滑曲線的終點有時會略微卷曲 - 這可以通過在第一個點的正下方和末尾添加一個初始點來修復。 這將迫使曲線具有更好的開始/結束。
  • 您可以繪制雙曲線以通過在另一個陣列上使用此列表中的第一個點但在弧頂部具有非常小的偏移來進行錐形效果(較薄的開始/結束,在中間較厚),然后將其渲染到頂部。

這個算法是為這個答案創建的,所以它顯然沒有經過適當的測試。 可能會有特殊情況和組合將其拋棄,但我認為這是一個良好的開端。

已知的弱點:

  • 它假設每個桿之間的距離對於斜率檢測是相同的。 如果距離在組內變化,則需要用基於因子的比較來替換。
  • 它將斜率與精確值進行比較,如果使用浮點值,則可能會失敗。 與epsilon /容差比較

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM