Как наилучшим образом приблизить геометрическую дугу к кривой Безье?
При рисовании дуги в 2D с использованием приближения кривой Безье, как рассчитать две контрольные точки, если у вас есть центральная точка окружности, начальный и конечный угол и радиус?
8 ответов
Это непросто объяснить в посте Stackru, особенно потому, что для его доказательства потребуется ряд подробных шагов. Однако то, что вы описываете, является распространенным вопросом, и есть ряд подробных объяснений. Смотрите здесь и здесь; Мне очень нравится #2, и я использовал его раньше.
Это 8-летний вопрос, но с которым я недавно боролся, поэтому я решил поделиться тем, что придумал. Я потратил много времени, пытаясь использовать решение (9) из этого текста, и не мог извлечь из него никаких разумных чисел, пока не попробовал Google и не узнал, что, по-видимому, в уравнениях были некоторые опечатки. В соответствии с исправлениями, перечисленными в этом сообщении, с учетом начальной и конечной точек дуги ([x1, y1] и [x4, y4], соответственно) и центра круга ([xc, yc]), можно Выведите контрольные точки для кубической кривой Безье ([x2, y2] и [x3, y3]) следующим образом:
ax = x1 – xc
ay = y1 – yc
bx = x4 – xc
by = y4 – yc
q1 = ax * ax + ay * ay
q2 = q1 + ax * bx + ay * by
k2 = 4/3 * (√(2 * q1 * q2) – q2) / (ax * by – ay * bx)
x2 = xc + ax – k2 * ay
y2 = yc + ay + k2 * ax
x3 = xc + bx + k2 * by
y3 = yc + by – k2 * bx
Надеюсь, что это помогает кому-то, кроме меня!
Хорошее объяснение приведено в разделе "Приближение" кривой Безье Куба круговыми дугами".
Короче говоря: используя кривые Безье, вы можете достичь минимальной ошибки 1,96×10^-4, что вполне нормально для большинства приложений.
Для положительной дуги квадранта используйте следующие точки:
p0 = [0, radius]
p1 = [radius * K, radius]
p2 = [radius, radius * K]
p3 = [radius, 0]
где K - это так называемое "магическое число", которое является нерациональным числом. Это может быть аппроксимировано следующим образом:
K = 0.5522847498
Я отвечаю на этот старый вопрос (который должен принадлежать математике, так что написание формул будет ужасно) с некоторыми демонстрациями.
Предположим, что P0 и P3 - ваша начальная и конечная точка вашей дуги, P1 и P2 - контрольные точки кривой Безье, а x - это мера угла, деленная на два. Предположим, что x меньше чем pi / 2.
Пусть PM средняя точка отрезка P0P3, а PH средняя точка дуги. Чтобы приблизить дугу, мы хотим, чтобы кривая Безье начиналась в P0, проходила через PH, заканчивалась в P3 и касалась дуги в P0 и P3.
(Нажмите "Выполнить фрагмент кода", чтобы отобразить рисунок. Проклятия, которые не будут поддерживаться, пока не поддерживают SVG.)
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 20 80 80">
<style>text{font-size:40%;font-style:italic;text-anchor:middle}tspan{font-size:50%;font-style:normal}</style>
<rect x="10" y="20" width="80" height="80" fill="none" stroke="gray"></rect>
<path stroke="gray" stroke-dasharray="3,2" fill="none" d="M25,30 62.6,31.62 80,65 22.19,95.13 25,30 80,65 M22.19,95.13 62.6,31.62"></path>
<path stroke="black" fill="none" d="M25,30A65.19 65.19 0 0 1 80,65"></path>
<circle r="1" fill="red" cx="25" cy="30"></circle>
<circle r="1" fill="green" cx="80" cy="65"></circle>
<circle r="1" fill="magenta" cx="22.19" cy="95.13"></circle>
<circle r="1" fill="darkgreen" cx="52.5" cy="47.5"></circle>
<circle r="1" fill="yellow" cx="57.19" cy="40.13"></circle>
<circle r="1" fill="maroon" cx="62.6" cy="31.62"></circle>
<circle r="1" fill="orange" cx="48.27" cy="31"></circle>
<circle r="1" fill="teal" cx="69.24" cy="44.35"></circle>
<text x="25" y="28">P<tspan>0</tspan></text>
<text x="48.27" y="29">P<tspan>1</tspan></text>
<text x="71.24" y="42.35">P<tspan>2</tspan></text>
<text x="83" y="63">P<tspan>3</tspan></text>
<text x="62.6" y="29.62">P<tspan>E</tspan></text>
<text x="59.19" y="47.13">P<tspan>H</tspan></text>
<text x="54.5" y="54.5">P<tspan>M</tspan></text>
</svg>
Пусть PE - пересечение прямых, касательных к дуге в P0 и P3. Чтобы кривая касалась дуги, P1 должен лежать на сегменте P0PE, а P2 должен лежать на P3PE. Пусть k будет отношением P0P1 / P0PE (также равным P3P2 / P3PE):
P1 = (1 - k) P0 + k PE
P2 = (1 - k) P3 + k PE
У нас также есть следующее (сделать некоторые пропорции):
PM = (P0 + P3) / 2
PH = PM / cos (x) = PM sec (x) = (P0 + P3) sec (x) / 2
PE = PH / cos (x) = PM сек (x) ^ 2 = (P0 + P3) сек (x) ^ 2/2
Чтобы упростить наши вычисления, я считал, что все векторные точки основаны на центре, но в итоге это не будет иметь значения.
Общая 4-точечная кривая Безье задается формулой
C (t) = t ^ 3 P3 + 3 (1 - t) t ^ 2 P2 + 3 (1 - t) ^ 2 t P1 + (1 - t) ^ 3 P0
Мы должны иметь C (1/2) = PH, поэтому
C (1/2) = (P0 + 3 P1 + 3 P2 + P3) / 8
= ((P0 + P3) + 3 (1 - k) P0 + 3 k PE + 3 (1 - k) P3 + 3 k PE) / 8
= ((P0 + P3) + 3 (1 - k) (P0 + P3) + 6 k PE) / 8
= (P0 + P3) (1 + 3 (1 - k) + 3 кс (x) ^ 2) / 8
Итак, это наше уравнение (умноженное на 8), чтобы найти k:
8 C (1/2) = 8 PH
=> (P0 + P3) (4 - 3 k + 3 k сек (x) ^ 2) = 4 (P0 + P3) сек (x)
Давайте избавимся от векторов (P0 + P3) и получим:
4 - 3 k + 3 k сек (x) ^ 2 = 4 сек (x)
=> 3 k (сек (х)^2 - 1) = 4(сек (х) - 1)
=> k = 4/3 (сек (х) + 1)
Теперь вы знаете, где разместить контрольные точки. Ура!
Если у вас есть x = pi / 4, вы получите k = 0,552... Возможно, вы видели это значение примерно.
При работе с эллиптическими дугами все, что вам нужно сделать, это соответствующим образом масштабировать координаты точек.
Если вам приходится иметь дело с большими углами, я предлагаю разделить их на несколько кривых. Именно это и делают некоторые программы при рисовании дуг, поскольку вычисление кривой Безье иногда быстрее, чем использование синусов и косинусов.
В Wolfram MathWorld есть код Mathematica: аппроксимация кривой Безье дуги, с которой следует начать.
Смотрите также:
Raphael 2.1.0 имеет поддержку Arc->Cubic (path2curve-function), и после исправления ошибки в нормализации S и T пути, похоже, теперь работает. Я обновил *Генератор случайных путей*, чтобы он генерировал только дуги, чтобы было легко проверить все возможные комбинации путей:
Проверьте, и если какой-то путь не пройден, я был бы рад получить отчет.
РЕДАКТИРОВАТЬ: Просто понял, что это 3-летняя тема...
Я имел успех с этим общим решением для любой эллиптической дуги в виде кубической кривой Безье. Он даже включает начальный и конечный углы в рецептуру, поэтому нет необходимости в дополнительном вращении (что может быть проблемой для некруглого эллипса).
Я наткнулся на эту проблему недавно. Я составил решение из статей, упомянутых здесь, в виде модуля.
Он принимает начальный угол, конечный угол, центр и радиус в качестве входных данных.
Он очень хорошо аппроксимирует маленькие дуги (<= PI/2). Если вам нужно что-то приблизить к дугам от PI / 2 до 2*PI, вы всегда можете разбить их на части Это решение не зависит от начального и конечного угловых порядков - оно всегда выбирает малую дугу. В результате вы получите все четыре точки, которые вам нужны, чтобы определить кубическую кривую Безье в абсолютных координатах. Я думаю, что это лучше всего объяснить в коде и комментариях:'use strict';
module.exports = function (angleStart, angleEnd, center, radius) {
// assuming angleStart and angleEnd are in degrees
const angleStartRadians = angleStart * Math.PI / 180;
const angleEndRadians = angleEnd * Math.PI / 180;
// Finding the coordinates of the control points in a simplified case where the center of the circle is at [0,0]
const relControlPoints = getRelativeControlPoints(angleStartRadians, angleEndRadians, radius);
return {
pointStart: getPointAtAngle(angleStartRadians, center, radius),
pointEnd: getPointAtAngle(angleEndRadians, center, radius),
// To get the absolute control point coordinates we just translate by the center coordinates
controlPoint1: {
x: center.x + relControlPoints[0].x,
y: center.y + relControlPoints[0].y
},
controlPoint2: {
x: center.x + relControlPoints[1].x,
y: center.y + relControlPoints[1].y
}
};
};
function getRelativeControlPoints(angleStart, angleEnd, radius) {
// factor is the commonly reffered parameter K in the articles about arc to cubic bezier approximation
const factor = getApproximationFactor(angleStart, angleEnd);
// Distance from [0, 0] to each of the control points. Basically this is the hypotenuse of the triangle [0,0], a control point and the projection of the point on Ox
const distToCtrPoint = Math.sqrt(radius * radius * (1 + factor * factor));
// Angle between the hypotenuse and Ox for control point 1.
const angle1 = angleStart + Math.atan(factor);
// Angle between the hypotenuse and Ox for control point 2.
const angle2 = angleEnd - Math.atan(factor);
return [
{
x: Math.cos(angle1) * distToCtrPoint,
y: Math.sin(angle1) * distToCtrPoint
},
{
x: Math.cos(angle2) * distToCtrPoint,
y: Math.sin(angle2) * distToCtrPoint
}
];
}
function getPointAtAngle(angle, center, radius) {
return {
x: center.x + radius * Math.cos(angle),
y: center.y + radius * Math.sin(angle)
};
}
// Calculating K as done in https://pomax.github.io/bezierinfo/#circles_cubic
function getApproximationFactor(angleStart, angleEnd) {
let arc = angleEnd - angleStart;
// Always choose the smaller arc
if (Math.abs(arc) > Math.PI) {
arc -= Math.PI * 2;
arc %= Math.PI * 2;
}
return (4 / 3) * Math.tan(arc / 4);
}
Быстрое решение на основе ответа @k88lawrence
Работает для дуг <= PI / 2
func controls(center: CGPoint, start: CGPoint, end: CGPoint) -> (CGPoint, CGPoint) {
let ax = start.x - center.x
let ay = start.y - center.y
let bx = end.x - center.x
let by = end.y - center.y
let q1 = (ax * ax) + (ay * ay)
let q2 = q1 + (ax * bx) + (ay * by)
let k2 = 4 / 3 * (sqrt(2 * q1 * q2) - q2) / ((ax * by) - (ay * bx))
let control1 = CGPoint(x: center.x + ax - (k2 * ay), y: center.y + ay + (k2 * ax))
let control2 = CGPoint(x: center.x + bx + (k2 * by), y: center.y + by - (k2 * bx))
return (control1, control2)
}