When drawing an Arc in 2D, using a Bezier Curve approximation, how does one calculate the two control points given that you have a center point of a circle, a start and end angle and a radius?
This isn't easily explained in a StackOverflow post, particularly since proving it to you will involve a number of detailed steps. However, what you're describing is a common question and there's a number of thorough explanations. See here and here; I like #2 very much and have used it before.
This is an 8-year-old question, but one that I recently struggled with, so I thought I'd share what I came up with. I spent a lot of time trying to use solution (9) from this article by Aleksas Riškus and couldn't get any sensible numbers out of it until I did some Googling and learned that, apparently, there were some typos in the equations. Per the corrections listed in this blog post, given the start and end points of the arc ([x1, y1] and [x4, y4], respectively) and the the center of the circle ([xc, yc]), one can derive the control points for a cubic Bézier curve ([x2, y2] and [x3, y3]) as follows:
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) * (sqrt(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
Hope this helps someone other than me!
This isn't easily explained in a StackOverflow post, particularly since proving it to you will involve a number of detailed steps. However, what you're describing is a common question and there's a number of thorough explanations. See here and here; I like #2 very much and have used it before.
A nice explanation is provided in "Approximation of a cubic bezier curve by circular arcs and vice versa"1, by Aleksas Riškus.
Long story short: using Bezier curves you can achieve a minimum error of 1.96×10^-4, which is pretty ok for most applications.
For a positive quadrant arc, use the following points:
p0 = [0, radius]
p1 = [radius * K, radius]
p2 = [radius, radius * K]
p3 = [radius, 0]
where K is a so-called "magic number", which is an non-rational number. It can be approximated as follows:
K = 0.5522847498
I'm answering to this old question (which should belong to Mathematics so writing the formulas is gonna be awful) with some demonstrations.
Suppose P0 and P3 are your initial and final point of your arc, P1 and P2 the control points of the Bézier curve, and x is the measure of the angle divided by two. Suppose x to be less that pi/2.
Let PM the midpoint of the segment P0P3 and PH the middle point of the arc. To approximate the arc, we want the Bézier curve to start in P0, pass through PH, end in P3, and be tangent to the arc in P0 and P3.
(Click on "Run code snippet" to show the figure. Curses to imgur still not supporting 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>
Let PE the intersection of the lines tangent to the arc in P0 and P3. In order for the curve to be tangent to the arc, P1 must lie on the segment P0PE, and P2 must lie on P3PE. Let k be the ratio P0P1/P0PE (also equal to P3P2/P3PE):
P1 = (1 - k)P0 + k PE
P2 = (1 - k)P3 + k PE
We also have the following (do some proportions):
PM = (P0 + P3) / 2
PH = PM / cos(x) = PM sec(x) = (P0 + P3) sec(x) / 2
PE = PH / cos(x) = PM sec(x)^2 = (P0 + P3) sec(x)^2 / 2
To simplify our computations, I've considered all vector points to be center-based, but in the end it won't matter.
The generic 4-points Bézier curve is given by the formula
C(t) = t^3 P3 + 3(1 - t)t^2 P2 + 3(1 - t)^2 t P1 + (1 - t)^3 P0
We must have C(1/2) = PH, so
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 k sec(x)^2) / 8
So, this is our equation (multiplied by 8) to find k:
8 C(1/2) = 8 PH
=> (P0 + P3)(4 - 3 k + 3 k sec(x)^2) = 4(P0 + P3) sec(x)
Let's get rid of the vectors (P0 + P3), and we get:
4 - 3 k + 3 k sec(x)^2 = 4 sec(x)
=> 3 k (sec(x)^2 - 1) = 4(sec(x) - 1)
=> k = 4 / ( 3 * (sec(x) + 1) )
Now you know where to place the control points. Hooray!
If you have x = pi/4, you'll get k = 0.552... You might have seen this value around.
When dealing with elliptic arcs, all you have to do is to scale the points' coordinates accordingly.
If you have to deal with larger angles, I suggest to split them in more curves. That's actually what some softwares do when drawing arcs, since computing a Bézier curve is sometimes faster than using sines and cosines.
There's Mathematica code at Wolfram MathWorld: Bézier Curve Approximation of an Arc, which should get you started.
See also:
Raphael 2.1.0 has support for Arc->Cubic (path2curve-function), and after fixing a bug in S and T path normalization, it seems to work now. I updated *the Random Path Generator* so that it generates only arcs, so it's easy test all possible path combinations:
Test and if some path fails, I'd be happy to get report.
EDIT: Just realized that this is 3 years old thread...
I've had success with this general solution for any elliptical arc as a cubic Bezier curve. It even includes the start and end angles in the formulation, so there's no extra rotation needed (which would be a problem for a non-circular ellipse).
I stumbled upon this problem recently. I compiled a solution from the articles mentioned here in the form of a module.
It accepts start angle, end angle, center and radius as input.
It approximates small arcs (<= PI/2) pretty well. If you need to approximate something arcs from PI/2 to 2*PI you can always break them in parts < PI/2, calculate the according curves and join them afterward.
This solution is start and end angle order agnostic - it always picks the minor arc.
As a result you get all four points you need to define a cubic bezier curve in absolute coordinates.
I think this is best explained in code and comments:
'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);
}
Swift solution based on @k88lawrence answer
Works for arcs <= 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)
}
© 2022 - 2024 — McMap. All rights reserved.