Как вычислить контрольные точки кривой Безье, которые избегают объектов?

в частности, я работаю в canvas с javascript.

в основном, у меня есть объекты, которые имеют границы, которые я хочу избежать, но все еще окружают кривой Безье. Однако я даже не уверен, с чего начать писать алгоритм, который будет перемещать контрольные точки, чтобы избежать столкновения.

проблема в изображении ниже, даже если вы не знакомы с нотной записью, проблема все равно должна быть довольно ясной. Точки кривой-красные точки!--1-->

кроме того, у меня есть доступ к ограничивающим коробкам каждой ноты, которая включает стержень.

enter image description here

поэтому, естественно, столкновения должны быть обнаружены между ограничивающими рамками и кривыми (некоторое направление здесь было бы хорошо, но я просматривал и вижу, что есть приличное количество информации об этом). Но что происходит после обнаружения столкновений? Что должно произойти, чтобы вычислить местоположения контрольных точек, чтобы сделать что-то похожее на:--1-->

enter image description here

2 ответов


подход Безье

первоначально вопрос является широким - возможно, даже широким, поскольку существует множество различных сценариев, которые необходимо учитывать, чтобы сделать "одно решение, которое подходит для всех". Это целый проект сам по себе. Для этого я представлю основа на решение, которое можно развивать - это не полное решение (но близко к одному..). В конце я добавил несколько предложений по дополнениям.

в основные шаги для этого решения:

сгруппируйте заметки в две группы, левую и правую части.

контрольные точки затем основаны на наибольшем угле от первой (конечной) точки и расстоянии до любой другой заметки в этой группе и последней конечной точки до любой точки во второй группе.

полученные углы из двух групп затем удваиваются (максимум 90°) и используются в качестве основы для расчета контрольных точек (в основном вращение точки). Расстояние может быть дополнительно обрезано с помощью значения натяжения.

угол, удвоение, расстояние, натяжение и отступы смещение позволит для тонкой настройки, чтобы получить лучший результат более всех. Могут быть особые случаи, которые нуждаются в дополнительных условных проверках, но это не относится к сфере охвата (это не будет полным готовым к ключу решением, но обеспечит хорошую основу для дальнейшей работы).

несколько снимков из процесс:

image2

image3

основной код в Примере разделен на два раздела, два цикла, которые анализируют каждую половину, чтобы найти максимальный угол, а также расстояние. Это может быть объединено в один цикл и иметь второй итератор для перехода справа на середину в дополнение к тому, который идет слева на середину, но для простоты и лучшего понимания того, что происходит, я разделил их на два цикла (и ввел ошибку во второй половине-просто знать. Оставлю это как упражнение):

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();

добавление эффекта сужения

создать кривую более визуально угождая сужать можно добавить просто путем делать следующее вместо этого:

вместо поглаживания пути после добавления первой кривой Безье отрегулируйте контрольные точки с небольшим смещением угла. Затем продолжите путь, добавив еще одну кривую Безье, идущую справа налево, и, наконец, заполните ее (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)

taper

скрипка

предложения по улучшению:

  • если расстояния обеих групп большие или углы крутые, их, вероятно, можно использовать как сумму для уменьшения напряжения (расстояния) или увеличения его (угла).
  • фактор доминирования / площади может повлиять на расстояния. Доминирование, указывающее, где самые высокие части смещены (лежит ли он больше в левой или правой стороне, и влияет на напряжение для каждой стороны соответственно). Это может быть / потенциально достаточно само по себе, но должно быть протестировано.
  • смещение угла конусности также должно иметь отношение к сумме расстояния. В некоторых случаях линии пересекаются и выглядят не очень хорошо. Сужение может быть заменено ручным подходом, анализирующим точки Безье (ручная реализация) и добавляющим расстояние между исходными точками и точками для возвращаемого пути в зависимости от массива позиция.

надеюсь, что это помогает!


кардинальный сплайн и фильтрационный подход

если вы открыты для использования подхода Безье, то следующее может дать приблизительную кривую над стеблями нот.

такое решение состоит из 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];

который будет представлен как это:

image1

затем я создал простой многопроходный алгоритм, который отфильтровывает провалы и точки На одном склоне. Шаги в алгоритме следующие:

  • хотя есть anotherPass (true) это будет продолжаться, или пока максимальное количество проходов изначально
  • точка копируется в другой массив до тех пор, пока skip флаг не установлен
  • затем он будет сравнивать текущую точку с next, чтобы увидеть, имеет ли она вниз-склон
  • если это так, он будет сравнивать следующую точку со следующей и посмотреть, имеет ли она уклон вверх
  • если это считается купанием и 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-обратите внимание, что примечание погружения игнорируется:

image2

после выполнения всех необходимых проходов конечный массив точек будет представлен как это:

image3

единственный оставшийся шаг-сгладить кривую. Для этого я использовал свою собственную реализацию кардинального сплайна (лицензированного под MIT и может быть найти здесь), который принимает массив с точками x, y и сглаживает его, добавляя интерполированные точки на основе значения напряжения.

он не будет генерировать идеальную кривую, но результат от этого будет быть:

image6

скрипка

есть способы улучшить визуальный результат, который я не рассматривал, но я оставлю его вам, чтобы сделать это, если вы чувствуете, что это необходимо. Среди них могли быть:

  • найдите центр точек и увеличьте смещение в зависимости от угла, чтобы он больше дуг вверху
  • конечные точки сглаженной кривой иногда слегка завиваются - это можно исправить, добавив начальная точка прямо под первой точкой, а также в конце. Это заставит кривую иметь более красивый старт / конец.
  • вы можете нарисовать двойную кривую, чтобы сделать эффект конусности (тонкое начало/конец, толще посередине), используя первую точку в этом списке на другом массиве, но с очень небольшим смещением в верхней части дуги, а затем отобразить ее сверху.

алгоритм был создан ad-hook для этого ответа, поэтому он, очевидно, не проверен должным образом. Там может будьте особыми случаями и комбинацией, но я думаю, что это хорошее начало.

известные недостатки:

  • предполагается, что расстояние между каждым стержнем одинаково для обнаружения наклона. Это необходимо заменить факторным сравнением в случае, если расстояние варьируется внутри группы.
  • он сравнивает наклон с точными значениями, которые могут потерпеть неудачу, если используются значения с плавающей запятой. Сравните с epsilon / tolerance