DrawArc and ellipse geometry (repost)

  • Thread starter Christopher Ireland
  • Start date
C

Christopher Ireland

Hello!

Using the definition for en ellipse (http://en.wikipedia.org/wiki/Ellipse) I
can draw an arc of points. However, the end points of this arc do not
coincide with the end points of an arc drawn with the DrawArc method, e.g.

public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}

private void Form2_Paint(object sender, PaintEventArgs e)
{
Draw(e.ClipRectangle, e.Graphics);
}

private void RectCenter(Rectangle r, out int x, out int y)
{
x = (r.Left + r.Right) / 2;
y = (r.Top + r.Bottom) / 2;
}

private PointF PointFromEllipse(Rectangle bounds, float degrees)
{
float a = bounds.Width / 2.0f;
float b = bounds.Height / 2.0f;
float rad = ((float)Math.PI / 180.0f) * degrees;

int xCenter, yCenter;
RectCenter(bounds, out xCenter, out yCenter);

float x = xCenter + (a * (float)Math.Cos(rad));
float y = yCenter + (b * (float)Math.Sin(rad));

return new PointF(x, y);
}

private void Draw(Rectangle rect, Graphics g)
{

rect.Inflate(-5, -5);
g.DrawArc(new Pen(Color.Green), rect, 0, 315);
PointF p;

for (int i = 0; i < 315; i++)
{
p = PointFromEllipse(rect, i);
g.DrawLine(new Pen(Color.Red), p, new PointF(p.X, p.Y + 2));
}
}
}

Can anybody please give me an explanation for this difference?
 
P

Peter Duniho

Using the definition for en ellipse
(http://en.wikipedia.org/wiki/Ellipse) I
can draw an arc of points. However, the end points of this arc do not
coincide with the end points of an arc drawn with the DrawArc method,

I'm not really clear on what your question is. You aren't drawing lines
that would form an arc. You are drawing vertical bars 2 pixels high at
various points around the ellipse, and that's exactly the output you get..

Here's a version of your code (minus the constant Form-derived class
implementation stuff) that corrects that along with some other problems
(you were failing to dispose of newly created pens correctly, for
example). It draws practically the same exact pixels as the DrawArc()
method, with only very minor variations that one would naturally expect
from two completely different implementations of drawing an ellipse
(especially when one implementation is actually just drawing straight line
segments between one degree intervals on the ellipse).


protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Draw(ClientRectangle, e.Graphics);
}

protected override void OnResize(EventArgs e)
{
base.OnResize(e);
Invalidate();
}

private void RectCenter(Rectangle r, out int x, out int y)
{
x = (r.Left + r.Right) / 2;
y = (r.Top + r.Bottom) / 2;
}

private PointF PointFromEllipse(Rectangle bounds, float degrees)
{
float a = bounds.Width / 2.0f;
float b = bounds.Height / 2.0f;
float rad = ((float)Math.PI / 180.0f) * degrees;

int xCenter, yCenter;
RectCenter(bounds, out xCenter, out yCenter);

float x = xCenter + (a * (float)Math.Cos(rad));
float y = yCenter + (b * (float)Math.Sin(rad));

return new PointF(x, y);
}

private void Draw(Rectangle rect, Graphics g)
{
rect.Inflate(-5, -5);

g.DrawArc(Pens.Green, rect, 0, 315);
PointF p, pPrev = PointFromEllipse(rect, 0);

for (int i = 1; i < 315; i++)
{
p = PointFromEllipse(rect, i);
g.DrawLine(Pens.Red, p, pPrev);
pPrev = p;
}
}
 
C

Christopher Ireland

Peter,
It draws practically the same exact pixels
as the DrawArc() method, with only very minor variations that one
would naturally expect from two completely different implementations
of drawing an ellipse (especially when one implementation is actually
just drawing straight line segments between one degree intervals on
the ellipse).

Thank you for your reply, Peter, and I'm sorry for not making myself
clearer. I've retouched your code to make my point:

protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Draw(ClientRectangle, e.Graphics);
}

protected override void OnResize(EventArgs e)
{
base.OnResize(e);
Invalidate();
}

private void RectCenter(Rectangle r, out int x, out int y)
{
x = (r.Left + r.Right) / 2;
y = (r.Top + r.Bottom) / 2;
}

private PointF PointFromEllipse(Rectangle bounds, float degrees)
{
float a = bounds.Width / 2.0f;
float b = bounds.Height / 2.0f;
float rad = ((float)Math.PI / 180.0f) * degrees;

int xCenter, yCenter;
RectCenter(bounds, out xCenter, out yCenter);

float x = xCenter + (a * (float)Math.Cos(rad));
float y = yCenter + (b * (float)Math.Sin(rad));

return new PointF(x, y);
}

private void Draw(Rectangle rect, Graphics g)
{
rect.Inflate(-5, -5);

using(Pen pen = new Pen(Color.Green, 3)) { //let's be good :)
g.DrawArc(pen, rect, 0, 315);
}
PointF p, pPrev = PointFromEllipse(rect, 0);

for (int i = 1; i <= 315; i++)
{
p = PointFromEllipse(rect, i);
g.DrawLine(Pens.Red, p, pPrev);
pPrev = p;
}
}
}

Here you can see that the green line doesn't reach as far around the arc as
the red line, despite the fact that both are, in theory, drawing an arc
through 315º. It is this effect, that the DrawArc method seems to draw
"short", of which I would be interested in hearing an explanation.
 
P

Peter Duniho

[...]
Here you can see that the green line doesn't reach as far around the arc
as
the red line, despite the fact that both are, in theory, drawing an arc
through 315º. It is this effect, that the DrawArc method seems to draw
"short", of which I would be interested in hearing an explanation.

Ahhh...I understand the question now.

The answer is that your ellipse function and what Windows does aren't the
same (obviously). More specifically, the algorithm you've used assumes
your ellipse was once a circle that's been squashed, and you are
designating the degrees in the coordinate space of the original perfectly
round circle. Of course, when you squash the circle in one dimension or
the other to get an ellipse, you wind up squashing your angles too.

Windows, on the other hand, is doing a true polar coordinate clipping of a
circle to obtain the arc. That is, the beginning and ending point of the
arc, specified in polar coordinates, assumes that you're really drawing an
ellipse, but designating the start and end points in an unmodified polar
coordinate space. That is, take the original ellipse, find where it
intersects with lines drawn from the center outward at the given angles,
and draw the arc between those lines.

IMHO, the Windows version is more mathematically correct, but I feel that
the main point is to decide which is more appropriate to your needs and
use it consistently. Obviously though, you can't mix and match without
seeing the discrepancy you're asking about.

For what it's worth, here's yet another version of your code (see below)
that I hopes makes it much clearer what's going on. I added a lot more
stuff, so that you can use the arrow keys to adjust the limits of your
arc, as well as drawing a perfectly round circle so that you can compare
the angles for that circle with the angles for the ellipse. In
particular, note that the version of the ellipse drawn with DrawArc()
aligns perfectly with the circle, while your ellipse calculate lags and
leads the circle depending on where in the arc you are.

Hope that helps.

Pete


Here's the code:

protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Draw(ClientRectangle, e.Graphics);
}

protected override void OnResize(EventArgs e)
{
base.OnResize(e);
Invalidate();
}

private float _degreesTotal = 360.0f;

protected override bool ProcessDialogKey(Keys keyData)
{
bool fHandled = false;

switch (keyData)
{
case Keys.Left:
_degreesTotal -= 1.0f;
fHandled = true;
break;
case Keys.Right:
_degreesTotal += 1.0f;
fHandled = true;
break;
}

if (fHandled)
{
if (_degreesTotal < 0)
{
_degreesTotal += 360.0f;
}
else if (_degreesTotal >= 360.0f)
{
_degreesTotal -= 360.0f;
}
Invalidate();
}

return base.ProcessDialogKey(keyData);
}

private Point RectCenter(Rectangle r)
{
return new Point((r.Left + r.Right) / 2, (r.Top + r.Bottom) /
2);
}

private PointF PointFromEllipse(Rectangle bounds, float degrees)
{
float a = bounds.Width / 2.0f;
float b = bounds.Height / 2.0f;
float rad = ((float)Math.PI / 180.0f) * degrees;

Point ptCenter = RectCenter(bounds);

float x = ptCenter.X + (a * (float)Math.Cos(rad));
float y = ptCenter.Y + (b * (float)Math.Sin(rad));

return new PointF(x, y);
}

private void Draw(Rectangle rect, Graphics g)
{
rect.Inflate(-5, -5);

Rectangle rectSquare;
Point ptCenter = RectCenter(rect);
int dxySquare = Math.Min(rect.Width, rect.Height);

rectSquare = new Rectangle(new Point(ptCenter.X - dxySquare /
2, ptCenter.Y - dxySquare / 2), new Size(dxySquare, dxySquare));

g.DrawLine(Pens.Black, rect.Location, new Point(rect.Left
+ rect.Width, rect.Top + rect.Height));
g.DrawLine(Pens.Black, new Point(rect.Left + rect.Width,
rect.Top), new Point(rect.Left, rect.Top + rect.Height));
g.DrawString("_degreesTotal: " + _degreesTotal.ToString(),
Font, Brushes.Black, 10.0f, 10.0f);

using (Pen penGreen = new Pen(Color.Green, 3.0f))
{
g.DrawArc(penGreen, rect, 0, _degreesTotal);
g.DrawArc(penGreen, rectSquare, 0, _degreesTotal);
}

PointF ptPrevSquare = PointFromEllipse(rectSquare, 0),
ptPrev = PointFromEllipse(rect, 0);

for (int i = 1; i <= _degreesTotal; i++)
{
PointF ptSquare = PointFromEllipse(rectSquare, i),
pt = PointFromEllipse(rect, i);
g.DrawLine(Pens.Red, pt, ptPrev);
g.DrawLine(Pens.Red, ptSquare, ptPrevSquare);
ptPrev = pt;
ptPrevSquare = ptSquare;
}
}
 
C

Christopher Ireland

Peter,
In particular, note that the version of the ellipse
drawn with DrawArc() aligns perfectly with the circle, while your
ellipse calculate lags and leads the circle depending on where in the
arc you are.

Thank you again for your time, Peter.

Yes, I noticed that affect as well.

Now then, what I'm after is to get a _correct_ point back from the ellipse
drawn by DrawArc when I pass PointFromEllipse a given angle. Given that the
wikipedia ellipse algorithm turns out to be a squashed circle, do you happen
to know what the actual windows (mathematically correct) algorithm is so I
can make an accurate calculation?

--
Thank you,

Christopher Ireland

"Growth for the sake of growth is the ideology of the cancer cell."
Edward Abbey
 
P

Peter Duniho

[...]
Now then, what I'm after is to get a _correct_ point back from the
ellipse
drawn by DrawArc when I pass PointFromEllipse a given angle. Given that
the
wikipedia ellipse algorithm turns out to be a squashed circle, do you
happen
to know what the actual windows (mathematically correct) algorithm is so
I
can make an accurate calculation?

Not off the top of my head, no. It should be a simple geometry problem
though. Possibly easier if solved in polar coordinate space than in
Cartesian.

Pete
 
C

Christopher Ireland

Peter,
Not off the top of my head, no. It should be a simple geometry
problem though. Possibly easier if solved in polar coordinate space
than in Cartesian.

Ok. I'm not sure it's that trivial.

I'll always have the alternative of drawing the windows arc to a graphics
path, grabbing the array of points and reading in the last one. Not as
elegant though :)

-
Thank you,

Christopher Ireland
 
P

Peter Duniho

Ok. I'm not sure it's that trivial.

Gracious...don't give up so easily.

The Wikipedia article on Ellipse has exactly the formula you want. And I
was right, in polar coordinates it's a pretty simple calculation (assuming
you've got the Wikipedia article to give you the exact formula :)...I'm
happy to not have to have derived it myself).

Try this:

private PointF PointFromEllipse(Rectangle rectBounds, float
degrees)
{
// Convert the basic input into something more usable
Point ptCenter = RectCenter(rectBounds);
float radians = ((float)Math.PI * degrees) / 180.0f;
float radiusH = rectBounds.Width / 2.0f,
radiusV = rectBounds.Height / 2.0f;

// Calculate the radius of the ellipse for the given angle
bool fXMajor = radiusH > radiusV;
float a = fXMajor ? radiusH : radiusV,
b = fXMajor ? radiusV : radiusH;
float eccentricity = (float)Math.Sqrt(1 - (b * b) / (a * a));
float radiusAngle = b / (float)Math.Sqrt(1 - (eccentricity *
eccentricity) * Math.Pow(Math.Cos(radians), 2));

// Convert the radius back to Cartesian coordinates
if (fXMajor)
{
return new PointF(ptCenter.X + radiusAngle *
(float)Math.Cos(radians),
ptCenter.Y + radiusAngle * (float)Math.Sin(radians));
}
else
{
return new PointF(ptCenter.X + radiusAngle *
(float)Math.Sin(radians),
ptCenter.Y + radiusAngle * (float)Math.Cos(radians));
}
}

Pete
 
P

Peter Duniho

Try this:

[function snipped]

Oops.

Well, there's a pretty significant bug, in that the routine doesn't handle
vertically-oriented ellipses (major axis parallel to the Y axis)
correctly. I tried to include that, but screwed it up and didn't bother
to test that case before posting.

I leave the fix as an exercise for the reader. It's not actually that
hard. :)
 
C

Christopher Ireland

Peter,
Gracious...don't give up so easily.

I'm sorry, I don't think I implied I was going to give up, did I?
The Wikipedia article on Ellipse has exactly the formula you want. And I
was right, in polar coordinates it's a pretty simple
calculation (assuming you've got the Wikipedia article to give you
the exact formula :)...I'm happy to not have to have derived it
myself).

Well, I guess if it was that trivial one would have got it right first time,
huh ;-)?
Try this:

Thank you again Peter, I'll see if I can make this work for me.
 
P

Peter Duniho

Well, I guess if it was that trivial one would have got it right first
time,
huh ;-)?

I don't think I used the word "trivial". The formula is simple, that's
all.

Still, you'd think one would, wouldn't you? :)

At least I recognized the need to deal with the case, even if I didn't
handle it correctly. That said, I botched it up well enough that it
occurs to me that if you start with the code I posted, you could spend a
while figuring out what needs to be taken out altogether.

I like leaving _something_ for the reader to figure out on their own, but
in this case that seems a little unfair. So, here's the correct function:

private PointF PointFromEllipse(Rectangle rectBounds, float
degrees)
{
// Convert the basic input into something more usable
Point ptCenter = RectCenter(rectBounds);
float radians = ((float)Math.PI * degrees) / 180.0f;
float radiusH = rectBounds.Width / 2.0f,
radiusV = rectBounds.Height / 2.0f;

// Calculate the radius of the ellipse for the given angle
bool fXMajor = radiusH > radiusV;
float a = fXMajor ? radiusH : radiusV,
b = fXMajor ? radiusV : radiusH;
float eccentricity = (float)Math.Sqrt(1 - (b * b) / (a * a));
float radiusAngle = b / (float)Math.Sqrt(1 - (eccentricity *
eccentricity) *
Math.Pow(fXMajor ? Math.Cos(radians) : Math.Sin(radians),
2));

// Convert the radius back to Cartesian coordinates
return new PointF(ptCenter.X + radiusAngle *
(float)Math.Cos(radians),
ptCenter.Y + radiusAngle * (float)Math.Sin(radians));
}
 
C

Christopher Ireland

Peter,
At least I recognized the need to deal with the case, even if I didn't
handle it correctly.

And I didn't know how to deal with the case, you're quite correct. That's
why I wrote to this group with the hope that somebody like you did :)
I like leaving _something_ for the reader to figure out on their own,
but in this case that seems a little unfair.
LOL!

So, here's the correct function:

That's perfect, thank you Peter.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top