我的第一个任务很简单:找到要在屏幕上绘制的椭圆点。我用下面的方法制作了一个Ellipse类,该方法接受0到2 * PI之间的角度并返回该点。
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);
}
}
我使用椭圆的参数方程式来完成此任务很方便。参数t是角度。计算X和Y值并将它们放在一起作为椭圆上的一个点。通过增加参数t,我可以按使绘制椭圆像连接点一样简单的顺序获得点。
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);
}
}
RunTest X 是我在Main中运行的方法。第一个会给我画椭圆的点。要点是正确的。 我可以通过绘制方法直观地确认椭圆被绘制到规范中,这里将不再介绍。 绘制不是问题。这里的要点是,对于t_rad的每个值,我在曲线上都有一个对应的点。
现在,在绘制椭圆后,我需要执行其他任务。为此,我需要反向处理,因为我在椭圆上取了一个任意点并将其转换回t_rad。 Math.Atan2应该可以解决问题。该方法称为GetTWhenPoint。这是MyMath类中的扩展方法。
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;
}
}
简单的三角函数,对吗?但是...
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);
}
当我使用它将点转换回t_rad2时,我希望它等于或近似于原始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
我在这里想念什么?到目前为止,我的所有数字都以弧度表示(据我所知)。现在这很奇怪...
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);
}
如果我将a和b设置为使椭圆成为一个完美的圆,那么一切看起来都很正常!
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
这告诉我的是,当我将点转换回t_rad2时,它会受到椭圆尺寸的影响。但是如何?除了椭圆相对于笛卡尔原点(0,0)的中心调整之外,GetTWhenPoint方法没有利用Ellipse类中的任何其他信息,特别是半轴。 Math.Atan2只需要该点的x和y值即可找到其与0度矢量所成的角度。这是基本的三角学。
它甚至不必关心椭圆上的一点。从方法的上下文来看,这就像无限地指向其他任何点一样。椭圆的尺寸如何影响我的扩展方式?
我的数学错了吗?我的意思是,自从使用Trig以来已经有一段时间了,但是我想我正确地记住了简单的方法。
谢谢!
答案 0 :(得分:2)
我想这就是你想要的。
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
}
}
我认为曲线的正确参数化是一条参数,其参数范围在0到1之间。因此不再需要指定弧度
x = A*Cos(2*Math.PI*t)
y = B*Sin(2*Math.PI*t)
和相反
t = Atan2(A*y, B*x)/(2*PI)
还要考虑相对于中心的极坐标中的椭圆形。
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)
另外,请考虑以下辅助函数,这些辅助函数将所需的象限(弧度形式)包裹角度
/// <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));
和学位版本
/// <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);