Math Installment #2: I needed some circular oriented text
[Sample Download on Project Distributor]
Okay, back with math installment #2... This time I needed some circular oriented text, or rather I needed some path oriented text, but that code is much harder so I'm starting with something a bit easier. There is quite a bit of math involved with laying out text that follows any sort of curve or line for that matter. A line is the easiest thing to lay out text on.
Linear Textual Layout
Laying out text to follow a line is one of the easier problems we can solve. A line is defined by a start point (Px1, Py1) and an end point (Px2, Py2). Now the lines are going to be oriented in GDI+ space which means Y increases down instead of up. This flips the quadrant layout of the normal Cartesian Coordinate plane but it won't affect us much. The first step for us now is going to be finding the angle of the line. This will be used to rotate the text before it is translated into the final position.
The angle of the line is found using some trig work on the slope of the line. We need the slope in two parts. The following equations point out the important items.
int dx = x2 - x1;
int dy = y2 - y1;
int m = dy/dx; // Classically (y2-y1)/(x2-x1)
Next we'll use the arc tangent in order to retrieve the angle in radians. Some optimizations can be made at this step. First, if x is 0, then either the angle is 90, 270 or 0 depending on dy. Our second check is for dy of 0. This either means the angle is either 0 or 180 depending on the value of dx. Finally if both dx and dy are greater than 0 we have some computations to do. If dx is less than 0 we find the arc tangent of the slope of the line, turn it into degrees and add 180 (otherwise we leave off the 180). Let's see what that looks like:
angle = atan(dx/dy) / pi * 180 + ((dx < 0) ? 180 : 0);
With the angle in hand, all we need is to set up the GDI+ rendering pipeline. GDI+ translations are done in the reverse order that they are popped onto the stack of matrix operations. That means we'll start with a TranslateTransform to the pointed identified by (x1, y1), and then a RotateTransform about the angle. The angle needs to be munged to operate with GDI+. That means taking the negative of the angle and adding 90. That'll get the unit circle angle in-line with the GDI+ rotation angle.
pe.Graphics.TranslateTransform(Px1, Py1);
pe.Graphics.RotateTransform((-angle) + 90);
pe.Graphics.DrawString("Line Text", this.Font, Brushes.Black, 0, 0);
pe.Graphics.ResetTransform();
Circular Textual Layout
The circular layout isn't much different, except that we already have our angle and we have to go backwards to get the location. Start out with what you are given. We need to first define the circle. We'll need a center point at (Cx, Cy), and a radius (Cr). Because we have to lay each textual character we'll need to offset it by some different angle each time. We fine this special angle by first computing the circumference of the circle using 2piCr (Cc) and then finding the arc distance for a single degree dividing by 360 (Carcd).
// Center of the circle
int Cx = ClientSize.Width / 2;
int Cy = ClientSize.Height / 2;
// Radius
int Cr = (int) (Cx * 0.9f);
// Circumference 2(Pi)(Cr)
float Cc = (float) (Math.PI * 2 * Cr);
// Arc Distance for 1 degree
float Carcd = Cc / 360f;
The text will be just big enough to fill a circle when the ClientSize is set to 400, 400. That is a Windows Forms thing and not part of the math. You need to start somewhere so the angle where we begin will be 135 (Tsangle). As we iterate through each character we take it's measurement first. This helps in the layout by giving us something to transform on before the rotation to make sure the text is rotated about its center. Next we get the coordinates where the character will appear. This is done using the sin and cosine functions about the angle.
radians = angle / 180 * PI
xoffset = cos(radians) * Cr
yoffset = sin(radians) * Cr
With that in hand you apply the transforms in reverse order. This time we want to translate the text to center, rotate it about center, then translate to final position. We have our offsets, the size of the text, our angle, and the center of the circle making this an easy task. Remember the transforms are in reverse order as to how they are applied.
pe.Graphics.TranslateTransform(Cx + x, Cy + y);
pe.Graphics.RotateTransform((-Tsangle) + 90);
pe.Graphics.TranslateTransform(-(thisChar.Width/2), -(thisChar.Height/2));
pe.Graphics.DrawString(T[i].ToString(), this.Font, Brushes.Black, 0, 0);
pe.Graphics.ResetTransform();
My mixture of code and math is probably confusing, but I've tried to take out as many implementation details as possible. Unfortunately the rendering process is dependent on GDI+ and the order of matrix operations. As each character is rendered you'll have to update the angle. Remember that Carcd knows exactly the arc length of a single degree. We can update the current angle by turning the width of the character into degrees based on the arc length, then subtracting this from the current angle. That moves us clockwise around the shape.
Tsangle -= (charWidth / Carcd);
A more proper way to do this might be to update the angle based not only on your character's width but also the previous character's width. That would allow a kind of conjugate offset allowing combinations of thin and wide characters to look smoother. Currently this algorithm doesn't show any major issues on sufficiently large circular regions, but there might be larger issues on smaller regions.
How can you go about improving this algorithm? Well, it already allows for an arbitrary start position and arbitrarily long text, but it doesn't support any sort of cut-off features. You might want to add those if the text is larger than the circle. You can implement line shifting so that the bottom or top of the text is aligned to the path circle. Currently if you render a circle or ellipse it will cut through the text. Even more fun is the GraphicsPath text renderer. However, it takes quite a bit more math including the calculation of normals, interpolation, and smoothing. Maybe I'll save that for another math tutorial. I welcome comments on ways to improve the discussion (maybe some diagrams?) and I'll post some test source if enough people are interested.