Noel Thadeus
Noel Thadeus

Reputation: 33

Find point on ellipse given the angle, then reverse the process

My first task is simple: find the points of the ellipse to be drawn on screen. I made an Ellipse class below with a method that takes in an angle between 0 to 2*PI and returns the point.

public class Ellipse
{
    public PointF Center { get; set; }
    public float A { get; set; }  /* horizontal semiaxis */
    public float B { get; set; }  /* vertical semiaxis */

    public Ellipse(PointF center, float a, float b)
    {
        this.Center = center;
        this.A = a;
        this.B = b;
    }        

    public PointF GetXYWhenT(float t_rad)
    {
        float x = this.Center.X + (this.A * (float)Math.Cos(t_rad));
        float y = this.Center.Y + (this.B * (float)Math.Sin(t_rad));
        return new PointF(x, y);
    }
}

I use the parametric equation of the ellipse as it is convenient for this task. Parameter t is the angle. X and Y values are calculated and put together as a point on the ellipse. By increasing parameter t, I can obtain the points in the order that makes drawing the ellipse as simple as connecting the dots.

private void RunTest1()
{
    PointF center = new PointF(0, 0);
    float a = 3;  /* horizontal semiaxis */
    float b = 4;  /* vertical semiaxis */
    Ellipse ellipse = new Ellipse(center, a, b);

    List<PointF> curve = new List<PointF>();   /* collects all points needed to draw the ellipse */

    float start = 0;
    float end = (float)(2 * Math.PI);  /* 360 degrees */
    float step = 0.0174533f;  /* 1 degree */

    for (float t_rad = start; t_rad <= end; t_rad += step)
    {
        PointF point = ellipse.GetXYWhenT(t_rad);
        curve.Add(point);
    }
}

RunTestX are methods I run in Main. The first will give me the points I need to draw this ellipse. The points are correct. I have visual confirmation of the ellipse being drawn to specification using a draw method that I will not be including here. Drawing it is NOT the issue. The takeaway here is that for every value of t_rad, I have a corresponding point on the curve.

Now I need to perform a different task after I draw the ellipse. To do this, I need to reverse the process in that I take an arbitrary point on the ellipse and convert it back to the t_rad. Math.Atan2 should do the trick. The method is called GetTWhenPoint. It's an extension method in MyMath class.

public static class MyMath
{
    public static float GetTWhenPoint(this PointF center, PointF point)
    {
        float x = point.X - center.X;
        float y = point.Y - center.Y;

        float retval = (float)Math.Atan2(y, x);
        if (retval < 0)
        {
            retval += (float)(2 * Math.PI);
        }

        return retval;
    }
}

Simple trigonometry, right? However...

private void RunTest2()
{
    PointF center = new PointF(0, 0);
    float a = 3;  /* horizontal semiaxis */
    float b = 4;  /* vertical semiaxis */
    Ellipse ellipse = new Ellipse(center, a, b);

    string debug = "TEST 2\r\n";

    float start = 0;
    float end = (float)(2 * Math.PI);  
    float step = 0.0174533f;      

    for (float t_rad = start; t_rad <= end; t_rad += step)
    {
        PointF point = ellipse.GetXYWhenT(t_rad);
        double t_rad2 = center.GetTWhenPoint(point);
        debug += t_rad.ToString() + "\t" + t_rad2.ToString() + "\r\n";
    }

    Clipboard.SetText(debug);
}

When I use it to convert the point back to t_rad2, I expect it to be equal to or pretty darn close to the original t_rad.

TEST 2
0   0
0.0174533   0.0232692267745733
0.0349066   0.0465274415910244
0.0523599   0.0697636753320694
0.0698132   0.0929670184850693
0.0872665   0.116126760840416
...
6.178444    6.14392471313477
6.195897    6.1670298576355
6.21335 6.19018936157227
6.230803    6.21339273452759
6.248257    6.23662853240967
6.26571 6.25988674163818
6.283163    6.28315591812134

What am I missing here? All my numbers so far have been in radians (as far as I can tell). Now here's where it gets weirder...

private void RunTest3()
{
    PointF center = new PointF(0, 0);
    float a = 4;  /* horizontal semiaxis */
    float b = 4;  /* vertical semiaxis */
    Ellipse ellipse = new Ellipse(center, a, b);

    string debug = "TEST 3\r\n";

    float start = 0;
    float end = (float)(2 * Math.PI);
    float step = 0.0174533f;  

    for (float t_rad = start; t_rad <= end; t_rad += step)
    {
        PointF point = ellipse.GetXYWhenT(t_rad);
        double t_rad2 = center.GetTWhenPoint(point);
        debug += t_rad.ToString() + "\t" + t_rad2.ToString() + "\r\n";
    }

    Clipboard.SetText(debug);    
}

If I set a and b equal to make the ellipse a perfect circle then everything looks normal!

TEST 3
0   0
0.0174533   0.0174532998353243
0.0349066   0.0349065996706486
0.0523599   0.0523599050939083
0.0698132   0.0698131918907166
0.0872665   0.0872664898633957
...    
6.178444    6.17844390869141
6.195897    6.19589710235596
6.21335 6.21335029602051
6.230803    6.23080348968506
6.248257    6.24825668334961
6.26571 6.26570987701416
6.283163    6.28316307067871

What this tells me is that when I convert the point back to t_rad2 it is somehow affected by the dimensions of the ellipse. But how? Apart from the center adjustment of the ellipse with respect to the Cartesian origin (0,0), GetTWhenPoint method does not make use of any other information from the Ellipse class specifically the semi-axes. Math.Atan2 only needs the x and y values of the point to find the angle it makes with the 0-degree vector. That's basic trigonometry.

It shouldn't even care that it's a point on the ellipse. From the context of the method, it's just a point just like infinitely any other. How is my extension method somehow being affected by the dimensions of my ellipse?

Is it my math that's wrong? I mean it's been a while since I used trig but I think I remember the simple ones correctly.

Thanks in advance!

Upvotes: 3

Views: 683

Answers (1)

John Alexiou
John Alexiou

Reputation: 29244

I think this is what you want.

public class Ellipse
{
    public PointF Center { get; set; }
    public float A { get; set; }  /* horizontal semiaxis */
    public float B { get; set; }  /* vertical semiaxis */

    public Ellipse(PointF center, float a, float b)
    {
        this.Center=center;
        this.A=a;
        this.B=b;
    }

    public PointF GetXYWhenT(float t_rad)
    {
        float x = this.Center.X+(this.A*(float)Math.Cos(t_rad));
        float y = this.Center.Y+(this.B*(float)Math.Sin(t_rad));
        return new PointF(x, y);
    }

    public float GetParameterFromPoint(PointF point)
    {
        var x = point.X-Center.X;
        var y = point.Y-Center.Y;

        // Since x=a*cos(t) and y=b*sin(t), then
        // tan(t) = sin(t)/cos(t) = (y/b) / (x/a)
        return (float)Math.Atan2(A*y, B*x);
    }
}

class Program
{
    static readonly Random rng = new Random();
    static void Main(string[] args)
    {
        var center = new PointF(35.5f, -12.2f);
        var ellipse = new Ellipse(center, 18f, 44f);

        // Get t between -π and +π
        var t = (float)(2*Math.PI*rng.NextDouble()-Math.PI);
        var point = ellipse.GetXYWhenT(t);

        var t_check = ellipse.GetParameterFromPoint(point);

        Debug.WriteLine($"t={t}, t_check={t_check}");
        // t=-0.7434262, t_check=-0.7434263
    }
}

I would consider the proper parametrization of a curve as one with a parameter that spans between 0 and 1. Hence the need to specify radians goes away

x = A*Cos(2*Math.PI*t)
y = B*Sin(2*Math.PI*t)

and the reverse

t = Atan2(A*y, B*x)/(2*PI)

Also, consider what the ellipse looks like in polar coordinates relative to the center.

x = A*Cos(t) = R*Cos(θ)     |  TAN(θ) = B/A*TAN(t)
y = B*Sin(t) = R*Sin(θ)     |  
                            |  R = Sqrt(B^2+(A^2-B^2)*Cos(t)^2)

                    A*B 
  R(θ) = ----------------------------
          Sqrt(A^2+(B^2-A^2)*Cos(θ)^2)

Also, consider the following helper functions that wrap angles around the desired quadrants (radian versions)

/// <summary>
/// Wraps angle between 0 and 2π
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
public static double WrapTo2PI(this double angle) 
    => angle-(2*Math.PI)*Math.Floor(angle/(2*Math.PI));
/// <summary>
/// Wraps angle between -π and π
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
public static double WrapBetweenPI(this double angle) 
    => angle+(2*Math.PI)*Math.Floor((Math.PI-angle)/(2*Math.PI));

and the degree versions

/// <summary>
/// Wraps angle between 0 and 360
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
public static double WrapTo360(this double angle) 
    => angle-360*Math.Floor(angle/360);
/// <summary>
/// Wraps angle between -180 and 180
/// </summary>
/// <param name="angle">The angle</param>
/// <returns>A bounded angle value</returns>
/// <remarks>see: http://stackoverflow.com/questions/7271527/inconsistency-with-math-round</remarks>
public static double WrapBetween180(this double angle)
    => angle+360*Math.Floor((180-angle)/360);

Upvotes: 2

Related Questions