[英]How to calculate bezier curve control points that avoid objects?
具體來說,我正在使用javascript在canvas中工作。
基本上,我的對象有我想要避免的邊界,但仍然圍繞着貝塞爾曲線。 但是,我甚至不確定從哪里開始編寫一個可以移動控制點以避免碰撞的算法。
問題出現在下圖中,即使您不熟悉音樂符號,問題仍然應該是相當清楚的。 曲線的點是紅點
此外,我可以訪問每個音符的邊界框,其中包括詞干。
所以很自然地,必須在邊界框和曲線之間檢測到碰撞(這里的某個方向會很好,但我一直在瀏覽並看到有相當數量的信息)。 但是在檢測到碰撞后會發生什么? 計算控制點位置以制作看起來更像的東西會發生什么:
最初的問題是一個廣泛的問題 - 甚至可能更廣泛的問題,因為有許多不同的情景需要考慮制定“一個適合所有人的解決方案”。 這是一個完整的項目。 因此,我將為您提供一個可以構建的解決方案的基礎 - 它不是一個完整的解決方案(但接近一個......)。 我在最后添加了一些關於添加的建議。
此解決方案的基本步驟是:
將筆記分為兩組,左側和右側。
然后,控制點基於從第一個(結束)點到該組中任何其他音符的最大角度,以及到第二個組中任何點的最后一個結束點。
然后將來自兩組的所得角度加倍(最大90°)並用作計算控制點(基本上是點旋轉)的基礎。 可以使用張力值進一步修剪距離。
角度,倍增,距離,張力和填充偏移將允許微調以獲得最佳的總體結果。 可能存在需要額外條件檢查的特殊情況,但這不在此范圍內(它不是完整的密鑰就緒解決方案,但為進一步工作提供了良好的基礎)。
來自該流程的幾個快照:
示例中的主要代碼分為兩部分,兩部分解析每一半以找到最大角度和距離。 這可以合並為一個循環,並且除了從左到中之外還有從右到中的第二個迭代器,但為了簡單起見並更好地理解發生了什么,我將它們分成兩個循環(並引入了一個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
建議的改進:
希望這可以幫助!
如果您打開使用非Bezier方法,則以下可以在音符桿上方給出近似曲線。
該解決方案包括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軸上的所有點之后 - 注意忽略傾斜音符:
在完成所有必要的傳遞之后,最終的點數組將表示為:
剩下的唯一步驟是使曲線平滑。 為此,我使用了我自己的基數樣條實現(在MIT下許可,可以在這里找到 ),它采用x,y點和平滑的數組,它根據張力值添加插值點。
它不會產生完美的曲線,但結果將是:
有一些方法可以改善我沒有解決過的視覺效果,但如果您覺得有必要,我會留給您做。 其中可能是:
這個算法是為這個答案創建的,所以它顯然沒有經過適當的測試。 可能會有特殊情況和組合將其拋棄,但我認為這是一個良好的開端。
已知的弱點:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.