How to move bezier curve points along circle to form a symmetric curve at center

How to move bezier curve points along circle to form a symmetric curve at center

From the image below, A is an open bezier curve with it's points positioned to represent a circle. AP3 and AP1 have the same position.
From B, How can I move the points (AP1 and AP3 along the bigger circle of radius r whiles the CP's reduce in length as CP1 and CP4 also rotate) till they form a symmetric bezier curve at the center of the bigger circle marked as the red curve?


Answer 1:

To keep things simple & consistent, let’s stick with circular arcs throughout.

We’ll have our first circle with radius \$r_1\$, shown in red below.

Then we’ll use a second radius \$r_2\$ to define the new positions of our endpoints as the curve swings open by an angle \$\alpha\$, shown on the green circle below.

Then we’ll draw a third circle, through our moved anchor points \$AP_1\$, \$AP_3\$ and the one we left stationary \$AP_2\$, shown in blue below. This gives us a unique arc to follow.

Diagram of osculating circles labelled with centers & radii

Since we already know how to trace a circular arc with a cubic Bézier curve, once we find this circle we’ll be able to draw the arc along it trivially. So I’ll focus on finding the parameters of the circle below:

Let’s get down to placing the points. For simplicity I’m going to assume that the fixed point is always at the rightmost edge of the red circle, and that the centers & radii of the circles \$ \vec C_1, r_1, \vec C_2,r_2\$ are known, along with the angle \$ \alpha > 0\$ you want to open the initial curve by. (For \$\alpha = 0\$, we just draw the original curve as in your previous question)

Now, we know our top anchor point is at…

$$\vec {AP_1} = \vec C_2 + r_2 \cdot \left(-\cos \alpha, \sin \alpha \right)$$

and our fixed anchor point is at…

$$\vec {AP_2} = \vec C_1 + \left( r_1, 0 \right)$$

Then the midpoint of the line joining these is at…

$$ \vec M = \frac 1 2 \cdot \left( \vec {AP_1} + \vec {AP_2} \right)$$

A perpendicular \$\vec p\$ from this midpoint will point the way to our third center \$\vec C_3\$

$$ \vec p = \left( -M_y, M_x – {AP_2}_x \right)$$

(If \$p_y = 0\$ then we’ve opened the curve all the way to a straight line, so just draw a straight line from \$AP_1\$ to \$AP_3\$ and you’re done)

We can scale this by a factor \$t\$ to reach the horizontal axis and locate our center:

$$t = \frac {{AP_2}_y – M_y} {p_y}$$

$$\vec C_3 = \vec M + t \cdot \vec p$$

$$r_3 = {AP_2}_x – {C_3}_x$$

And now we can find our angle \$\theta\$ with:

$$\theta = atan2(\vec {AP_1} – \vec C_3)$$

(Just note that in many APIs, the first argument of atan2 is y not x, so be sure you put the components in the right order)

Now we have a center (\$\vec C_3\$), a radius (\$r_3\$), a start angle (0), and an end angle (\$\theta\$), so we can draw a cubic Bézier curve along this circular arc using the technique described in my earlier answer.

For the lower half, just flip the y coordinates along the horizontal.

Here’s an animation of this method in action:

Animation showing inner circle opening out to a line then re-closing on the opposite side

With a little attention to our start points, we can even make this work starting from a less-than-full circle:

Animation showing a partial circular arc opening out in the same way

Here’s the code I used:

// Buffer for our final spline's anchor & control points.
Vector2[] _bezierPoints = new Vector2[7];

// How much of the inner circle should we start with?
public float circleCoverageDegrees= 180f;

// Size & center of our inner circle (red).
public float firstRadius = 1.0f;
public Vector2 firstCenter;

// Size & center of our second circle (green).
public float secondRadius = 2.0f;
Vector2 _secondCenter;  // Computed from radius & coverage.

// How far should we swing the anchor points out around the green circle?
[Range(0, 180)]
public float swingOutDegrees = 0f;

// Size & center of our third circle (blue).
Vector2 _thirdCenter;
float _thirdRadius;

// Used to mark the line from the second circle's center
// to our inner anchor points before we've swung them outward.
Vector2 _secondToInnerStart;

void OnValidate()
    if (secondRadius < firstRadius)
        secondRadius = firstRadius;

    // Place the anchor points of the original circle.
    // (For now, I assume its center is (0, 0), and add it as an offset later)
    Vector2 innerArcStart = PointOnCircle(firstRadius, circleCoverageDegrees);
    Vector2 fixedAnchor = PointOnCircle(firstRadius, 0f);

    // Place the second center so the green circle passes through the endpoints.
    float horizontalDeviation = Mathf.Sqrt(secondRadius * secondRadius - innerArcStart.y * innerArcStart.y);
    _secondCenter = new Vector2(innerArcStart.x + horizontalDeviation, 0f);

    _secondToInnerStart = innerArcStart - _secondCenter;

    float startDegrees = Mathf.Approximately(_secondToInnerStart.x, 0) ? 90f 
                      : Mathf.Rad2Deg * Mathf.Atan(-_secondToInnerStart.y / _secondToInnerStart.x);

    // Swing out our anchor points along the green circle.
    Vector2 finalArcStart = _secondCenter + PointOnCircle(secondRadius, 180 - startDegrees - swingOutDegrees);      

    // Find the center of the blue circle joining these anchors.
    Vector2 midpoint = (finalArcStart + fixedAnchor) / 2f;
    Vector2 perpendicular = new Vector2(-midpoint.y, midpoint.x - fixedAnchor.x);

    // If they're in a vertical line, abort and just draw a straight line.
    if(Mathf.Approximately(perpendicular.y, 0f)) {
        _thirdRadius = float.PositiveInfinity;

        _bezierPoints[0] = firstCenter + finalArcStart;
        _bezierPoints[3] = firstCenter + fixedAnchor;
        _bezierPoints[6] = 2 * firstCenter - finalArcStart;

        _bezierPoints[1] = Vector2.Lerp(_bezierPoints[0], _bezierPoints[3], 1f / 3f);
        _bezierPoints[2] = Vector2.Lerp(_bezierPoints[0], _bezierPoints[3], 2f / 3f);
        _bezierPoints[4] = Vector2.Lerp(_bezierPoints[3], _bezierPoints[6], 1f / 3f);
        _bezierPoints[5] = Vector2.Lerp(_bezierPoints[3], _bezierPoints[6], 2f / 3f);

    // Phew, we have a non-infinite circle! Place its center & radius.
    float t = -midpoint.y / perpendicular.y;

    _thirdCenter = new Vector2(midpoint.x + t * perpendicular.x, 0f);
    _thirdRadius = Mathf.Abs(_thirdCenter.x - fixedAnchor.x);

    // Find the angle of the endpoints around this blue circle.
    Vector2 thirdToFinalStart = finalArcStart - _thirdCenter;
    float angle = Mathf.Rad2Deg * Mathf.Atan2(thirdToFinalStart.y, thirdToFinalStart.x);

    // Handle reversing concavity correctly.
    float pivot = _thirdCenter.x < fixedAnchor.x ? 0 : 180;

    // Populate our Bezier curve buffers using code from previous answer.
    BezierCircle(_thirdCenter, _thirdRadius, angle, pivot, _bezierPoints, 0);
    BezierCircle(_thirdCenter, _thirdRadius, pivot, 2 * pivot - angle, _bezierPoints, 3);

// Convenience method for plotting a point in polar coordinates.
static Vector2 PointOnCircle( float radius, float angleDegrees) {
    float angleRadians = angleDegrees * Mathf.Deg2Rad;
    return radius * new Vector2(Mathf.Cos(angleRadians), Mathf.Sin(angleRadians));