Rectangles, Rounded Rectangles and Ellipses in Windows can only be drawn by GDI as axis-aligned figures. If one wishes to draw rotated or skewed figures under Windows NT, one can use World Transforms but they are unfortunately not available under Windows 95/98. For a cross-platform solution, one needs to do more of the work oneself. Rectangles can be simulated trivially using polygons with four vertices, which can then be rotated or skewed. However, what is one do for ellipses? There are basically three choices:
- Use a custom function to draw the ellipse.
The mathematics for ellipses are relatively simple and there are modified Bresenham equations for rotated ellipses in standard texts. However, this means that one must perform the rasterization oneself, which can get complicated for thick lines. This amount of effort is probably only worthwhile if one is already drawing to an off-screen surface (e.g. DirectDraw) or a bitmap.
- Drawing ellipses as connected line segments.
The actual lines can be drawn via LineTo(...) or Polyline(...) GDI calls. One can perform the approximation of the ellipses oneself or take advantage of the GDI FlattenPath(...) function.
- Use cubic bezier curves to approximate ellipses.
This is the method illustrated here.
Drawing ellipses as connected bezier curves
Using just four bezier curves, each representing 90 degrees of the original axis-aligned ellipse, one can arrive at a fair approximation with a maximum error of just 0.027%. This is equivalent to a maximum error of less than one pixel for ellipses with radii up to 3700, more than enough accuracy for our purposes.
- Device independence.
One can take advantage of hardware support of curve drawing on newer video cards. On my system, this is as fast as, if not faster than, GDI calls to Ellipse(...)
Because bezier curves are invariant under rotation, scaling and translation, one only needs to transform the control points to apply the same transformations to one's ellipse. More precisely, since each point on a cubic bezier curve is a barycentric combination of the control points, the relationship of the curve to the control points is not changed under affine maps.
If one is converting ellipses to line segments or rasterizing oneself, each time the resolution of one's surfaces or device context changes (for example when drawing to a printer DC), one must re-rasterize. This is not necessary with bezier curves. An additional advantage is that the ellipses can be exported via metafiles to drawing programmes such as CORELDRAW, which can then scale the figures without gross distortions or artefacts.
First start off with a rectangle bounding an axis aligned ellipse (as in a normal GDI call). The thirteen bezier control points (labelled 0-12 below) defining the four bezier curves making up the ellipse can be calculated easily using an empirically derived "magical constant". The following code produces the control points for mapping modes where +ve Y is downwards (e.g. MM_TEXT). Where the opposite is true, just have a negative y value for the offset variable as indicated in the comments.
// Create points to simulate ellipse using beziers
void EllipseToBezier(CRect& r, CPoint* cCtlPt)
// MAGICAL CONSTANT to map ellipse to beziers
const double EToBConst = 0.2761423749154;
CSize offset((int)(r.Width() * EToBConst), (int)(r.Height() * EToBConst));
// Use the following line instead for mapping systems where +ve Y is upwards
// CSize offset((int)(r.Width() * EToBConst), -(int)(r.Height() * EToBConst));
CPoint centre((r.left + r.right) / 2, (r.top + r.bottom) / 2);
cCtlPt.x = //------------------------/
cCtlPt.x = // /
cCtlPt.x = // 2___3___4 /
cCtlPt.x = r.left; // 1 5 /
cCtlPt.x = // | | /
cCtlPt.x = // | | /
cCtlPt.x = r.right; // 0,12 6 /
cCtlPt.x = // | | /
cCtlPt.x = centre.x - offset.cx; // | | /
cCtlPt.x = // 11 7 /
cCtlPt.x = centre.x + offset.cx; // 10___9___8 /
cCtlPt.x = // /
cCtlPt.x = centre.x; //------------------------*
cCtlPt.y = r.top;
cCtlPt.y = r.bottom;
cCtlPt.y = centre.y + offset.cy;
cCtlPt.y = centre.y - offset.cy;
cCtlPt.y = centre.y;
// LDPoint is an equivalent type to CPoint but with floating point precision
void Rotate(double radians, const CPoint& c, CPoint* vCtlPt, unsigned Cnt)
double sinAng = sin(radians);
double cosAng = cos(radians);
LDPoint constTerm( c.x - c.x * cosAng - c.y * sinAng,
c.y + c.x * sinAng - c.y * cosAng);
for (int i = Cnt-1; i>=0; --i)
vCtlPt[i] = (LDPoint( vCtlPt[i].x * cosAng + vCtlPt[i].y * sinAng,
-vCtlPt[i].x * sinAng + vCtlPt[i].y * cosAng) + constTerm).GetCPoint();
// Create Ellipse
CRect rect; GetClientRect(&rect);
Rotate(m_Radians, midPoint, ellipsePts, 13);
Of course, four bezier curves together only make up the outline of an ellipse, whether rotated or not. Thankfully, Win32 Path functions are there for filled ellipses. One only needs to enclose the PolyBezier(...) call in a Path Bracket. The resulting path can be stroked and filled to one's satisfaction. If one is feeling adventurous, further special fills like gradients, custom bitmaps or fractals etc. can be achieved by first setting the clipping region to the path via SelectClipPath(...).
// or FillPath();
// or StrokeAndFillPath();
// or PathToRegion(dc.m_hDC);
Thick Dashed or Dotted Ellipses Outlines under Win95/8
Win95/8 only supports solid thick lines. However, dashed or dotted ellipse outlines can easily be simulated with a series of bezier segments.