Math Installment #3: A quick look at wavy text
[Sample Download on Project Distributor]
I previously covered laying out text along a linear path and a circular path. You can view that code here Math Installment #2: I needed some circular oriented text. I'll be referencing the code so you really need to check it out if you haven't. Not to mention there are some great diagrams explaining the process. Adding the ability to align text to a curvy or wavy path requires some additional math and some new insights over the basic circular and linear layouts covered earlier. You'll find that GDI+ transformations really help even if there is quite a bit of code left to write.
Wavy Text
Wavy text consists of a line along which the text is going to be oriented. The line is defined as before with a start and end point. We'll also be grabbing that angle again and applying a transformation to align the final text to the direction of the line. In addition to the linear layout process, you'll have to additionally use a circle or period to describe the wave. Since we already have the code for the circle we'll use the radius to define period and compute the circumference and arc radius distance for layout purposes. All of this is covered in that last article, so read up.
Wavy text has a number of layout requirements. First you have to transform and rotate each character according to the location on the wave where it appears. We'll use the same angle features as before where we update the angle based on the arc distance and width of the text. To orient the text we have to realize that the rotation is going to oscillate between -90 and 90 degress depending on the location upon the curve. With circular text the angle of rotation covers a full 360 degrees, not just the 180 degress that occurs when fitting to a wavy line. To keep the range in bounds we'll use the cosine function.
cos(180) = -1
cos(0) = 1
// The cosine function alternates between -1 and 1
cos(angle) * 90
// This bounds the oscillation between -90 to +90
With the rotation complete we have to orient the text in the appropriate location. This is not a circle and so some special layout logic has to be used to keep the text moving along the x-axis. First we compute the base distance. This is the current angle divided by 180. For every 180 degrees we've traveled we need to add Cr * 2 to the base offset. Without this the text won't move across the x-axis and will just bunch up and overwrite in place.
baseX = floor(realAngle / 180) * Cr * 2
Now, depending on if the wave is currently in a crest or trough we have to switch how we compute the wave or crest local x offset. If the current angle is less than 180 degrees (this will be a trough) then we take the Cos of the angle and then subtract it from 2 and multiply by the current radius. For the crest regions we forego the offset by 2.
localX = (2 - (cos(radians) + 1)) * Cr
localX = (cos(radians) + 1) * Cr
// Edit - Clamping radians removes this problem
cradians = (angle % 180) / 180f * Math.PI;
localX = (2 - (cos(cradians) + 1)) * Cr
The final transformation is (baseX + localX, sin(radians) * Cr). At this point you would have wavy text along the x-axis. We want the wavy text to be aligned to an arbitrary axis so we need to rotate about the angle of the original line which has already been computed based on the starting and ending points. The last offset moves our transformed and rotated text to the starting offset of the line. This is a fairly complex orientation. Follows is a bit of code that puts a GDI+ face on the entire process.
// Translate to the line position
pe.Graphics.TranslateTransform(Px1, Py1);
// Rotate to bring in line with the line
pe.Graphics.RotateTransform(angle);
// Place the item on the X axis based on the current angle
// Use the Sin to find the Y axis location
float xOffset = ((int) (realAngle / 180)) * Cr * 2;
float curvePlacement = 0;
if ( realAngle % 360 < 180 ) {
curvePlacement = (float) ((2 - (Math.Cos(radians) + 1)) * Cr);
} else {
curvePlacement = (float) ((Math.Cos(radians) + 1) * Cr);
}
pe.Graphics.TranslateTransform(
(float) xOffset + curvePlacement,
(float) (Math.Sin(radians) * Cr)
);
// Rotate the text about the appropriate transform
pe.Graphics.RotateTransform((float) (Math.Cos(radians) * 90f));
// Center the Text
pe.Graphics.TranslateTransform(-(thisChar.Width/2), -heightOffset);
pe.Graphics.DrawString(T[i].ToString(), this.Font, Brushes.Black, 0, 0);
pe.Graphics.ResetTransform();
There are many different improvements that could be made to this algorithm. Many of the computations could be made simpler by incremental arithmetic. For the most part the computations made for each character do make the algorithm quite robust to odd combinations of period and font-size. I'm not sure covering all your bases here is going to be nearly that important though.