Circular Text In Flex Explained

I've gotten a bit distracted from my circular text project and have yet to rebuild it as true, well-formed component; I'm still planning to get around to this. However, I did say that I would publish a bit of the theory behind it, so what the heck, here we go.

First, this is going to be a Gumbo/Flex 4 application. We could do this in an earlier version, but we would have to embed fonts. To keep this simpler, and smaller, we're going to use the new framework. This also means you'll need Flash Player 10 to see the text. Literally. In previous versions of Flash, rotated system fonts are not rendered. So, to move along, make sure you are using a recent version of the Flex 4 framework and are viewing your results in some revision of Flash Player 10. (If anyone would like, I'll go over how to use Gumbo/Flex 4 in Flex Builder and/or FlashDevelop, just post a comment requesting it)

Let's Start With The Text

So you've got some text that you want to draw onto the screen. Something like:

var myText:String = "Read my blog @ http://blog.ShortFusion.com";

Self-serving, you betcha, but still, it's a string and we have to write something. So with this string, we want to iterate/loop over the individual characters and place them in the desired container. Note that I did investigate the idea of just taking the string as a whole, and then bending it into a curve, but the overall effect wasn't what I was hoping to get. Check out my previous article on using displacement maps to curve text in flex for more details. Okidoki, looping over the text:

for (var char:uint=0;char<myText.length;char++) {
tempText:TextView = new TextView();
tempText.text = myText.charAt(char);
addChild(tempText);
}

That works, but the results are not that useful. The result from FlexBuilder is a stacking of all the characters on top of one another, and in FlashDevelop the result is a vertical stream of the characters running down your page (I'll try to make some time to investigate why FlashDevelop does that as the FlexBuilder result seems more intuitive to me). What we need to do is set the x and y properties as we iterate. At the same time, I am going to set the rotation property to some arbitrary non-zero number. I did this specifically to make sure that my FlashDeveloper settings were correct and so that I wouldn't have to tackle framework problems further into the code. During earlier experiments I tripped myself up over this a few times.

One other thing, also to get around an issue I had in FlashDevelop, but since it's desirable to have anyway, I created a canvas into which I put all my characters. Our code now looks something like this:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script>
<![CDATA[

import mx.components.TextView;
        
public var myText:String = "Read my blog @ http://blog.ShortFusion.com";
        
public function drawText():void {
for (var char:uint=0;char<myText.length;char++) {
var tempText:TextView = new TextView();
tempText.x = 20 + (10 * char);
tempText.y = 20;
tempText.rotation = 25;
tempText.text = myText.charAt(char);
textContainer.addChild(tempText);
}
}
        
]]>

</mx:Script>

<mx:Button label="Draw a circle" click="drawText()" />
<mx:Canvas id="textContainer" y="50" x="50" width="300" height="300" />        

</mx:Application>

If you compile the project and click the button, you wind up with the following

flex app showing angled text on a straight line

That's nice and all, but what we want to do is to wrap the characters on a curve. Moving along then, the next thing to do is to reset the our rotation to zero; we'll come back to it once we have the whole cicle thing going.

tempText.rotation = 0;

Ok, It's Math Time

In the end, all you need to know are the equations, and I'll give them to you. However, there is a really handy rule you ought to know (and maybe remember from high school trig) and that is the mnemonic SOHCAHTOA. This helps when you are looking at a right triangle and want to know solve for one of the unknowns. So, here's a right triangle that is labeled for our mnemonic.

right triangle with labeled sides and angle

We have a right triangle with three labeled sides: Hypotenuse, Opposite and Adjacent. We have also identified an angle, A. When I mention a side, I will be refering to its length. The relationships that we care about are as follows:

  • Sine(A) = Opposite/Hypotenuse
  • Cosine(A) = Adjacent/Hypotenuse
  • Tangent(A) = Opposite/Adjancent

[If you want to test this, remember that the relationship between lines in a triangle is Adjacent^2+Opposite^2 = Hypotenuse^2 so just assume Adjacent = Opposite = 1 and therefore Hypotenuse = 2^0.5. Now, use a calculator and run the equations. It's freakin' magic!]

This gives us three relationship: SineOppositeHypotenuse (SOH), CosineAdjacentHypotenuse (CAH), TangentOppositeAdjancent (TOA) => SOHCAHTOA. I find this a really handy device for remembering the relationships of the right triangle. Additionally, keep in mind that, for purposes of dealing with a circle, Hypotenuse = Radius

circle containing rigth triangle with hypotenuse equal to the radius

The last little bit of math to consider is that the set of parametric equations that describe a circle (that interests us right now) is:

x = r*cos(t)
y = r*sin(t)

Where r = radius, and t is the angular offset from the positve x-axis.

For a brief description with animated gif from people who have a superior knowlege of math than I do, check out Parametric Equations from MathWorld, A Wolfram Web Resource (they make Mathematica).

Let's Curve Some Text Already!

Since we want to map around some circle, we need a radius. In my previous example of circular text in flex blog, I used a slider. To simplify things, we'll just use a static value:

var radius:Number = 125;

The next thing to consider is that even though the rotation property takes degrees, all the trig functions work in radians. So, we need to give ourselves a convenient way to bounce from degrees to radians. If you remember that 2*Pi*Radians = 360 Degrees, the following little function will be easy to understand:


private function degrees2radians(deg:Number):Number {
     return (2 * Math.PI * deg) / 360; 
}

Next, we need some spacing between characters. This will be something worthy of tweaking later on, but for now, let's just say 8 degrees between characters.

var spacing:Number = 8;

Additionally, we are kind of close to the top left corner. An easy way to move it is to add an offset to the x and y values. This offset is actually the center of the circle. There are several ways to define this, but just for illustrative purposes, and because it makes sense, I am going to create an instance of the Point class and use that for my center:

var center:Point = new Point(125,125);

You can use whatever values you want, but my center is going to sit at 125,125 so I have a little room to work with.

So let's modify our original program to implement these changes:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script>
<![CDATA[

import mx.components.TextView;
        
public var myText:String = "Read my blog @ http://blog.ShortFusion.com";
var radius:Number = 125;
var spacing:Number = 8;
var center:Point = new Point(125,125);
        
public function drawText():void {
for (var char:uint=0;char<myText.length;char++) {
var tempText:TextView = new TextView();
tempText.x = radius * Math.cos(degrees2radians(char*spacing)) + center.x;
tempText.y = radius * Math.sin(degrees2radians(char*spacing)) + center.y;
tempText.rotation = 25;
tempText.text = myText.charAt(char);
textContainer.addChild(tempText);
}
}
        
]]>

</mx:Script>

<mx:Button label="Draw a circle" click="drawText()" />
<mx:Canvas id="textContainer" y="50" x="50" width="400" height="400" />        

</mx:Application>
One thing that I didn't mention that you'll probably notice is that I am multiplying the spacing by char to determine the angle. spacing is measured against the axis, so each character needs to be progresively further away, otherwise we'll wind up with the characters stacking on top of one another.

So, if you used my example string and the values above, you'll wind up with circular text:

screenshot of circular text in flex

Ok, major accomplishment that's pretty cool. Of course, it would be better if our text angled into the circle and maybe we don't want the text to begin at the right x-axis. character 'A' sitting on tangent line First things first, let's tilt our text as it moves around the circle so that it is parallel with a line tangental to its place on the circumference. That looks something like the image to the right.

We need to know what angle we need to rotate our character by so that it matches the angle of the tangent line for its x and y value. So, what do we know? Well, we know the x and y value of the character because we just set it. We also know the center of the circle. And, we have this handy SOHCAHTOA memory device. So, we can reduce this down to a triangle where the length of the Adjacent side is the center.x - tempText.x and the length of the opposite side is center.y - tempText.y. Having the Opposite and Adjacent sides, we look back and see that Tangent(A) = Opposite/Adjacent so the andle of interest is going to be the arctangent of Opposite/Adjacent:

var dx:Number = center.x-tempText.x; var dy:Number = center.y-tempText.y; var angle:Number = Math.atan2(dy, dx);

We use the atan2 function as it returns a positive angular value for angles between 0 and 180 degrees.

There's a problem with this, however. What this gives us is the angle from the origin (center) to the letter, but what we need is the angle of rotation. These are complementary angles (umm, not exactly, more like negative complementary angles) so the solution is to subtract 90 from the angle that we calculate. And, again, since the function returns radians, and the rotation property wants degrees, we need to create a translation function going the other way:

private function degrees2radians(deg:Number):Number {
     return rad * 180 / Math.PI; 
}

So, this is where we are at:


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script>
<![CDATA[

import mx.components.TextView;
        
public var myText:String = "Read my blog @ http://blog.ShortFusion.com";
        
public function drawText():void {            
var radius:Number = 125;
var spacing:Number = 8;
var center:Point = new Point(125,125);
            
for (var char:uint=0;char<myText.length;char++) {
var tempText:TextView = new TextView();
tempText.x = radius * Math.cos(degrees2radians(char*spacing)) + center.x;
tempText.y = radius * Math.sin(degrees2radians(char*spacing)) + center.y;
var dx:Number = center.x-tempText.x;
var dy:Number = center.y-tempText.y;
var angle:Number = Math.atan2(dy, dx);
tempText.rotation = radians2degrees(angle) -90;
tempText.text = myText.charAt(char);
textContainer.addChild(tempText);
}
}
        
private function degrees2radians(deg:Number):Number {
return (2 * Math.PI * deg) / 360;
}
        
private function radians2degrees(rad:Number):Number {            
return rad * 180 / Math.PI;
}
        
    ]]>

</mx:Script>
<mx:Button label="Draw a circle" click="drawText()" />
<mx:Canvas id="textContainer" y="50" x="50" width="400" height="400" />        
</mx:Application>

And that gives us the following:

circular, rotated text in flex

One last little problem with the rotation. The origin of the character is in the top left corner, however, what we really want is for the text to be rotated through its center. The easiest way to resolve this issue is to "fudge" it. That is, we will lean the text character a few extra degrees into the curve in order to better approximate the correct axis of rotation (I'm looking, though admittedly not very energetically, for a better resolution to this.) Let's create a new variable called angleLeading to account for this:

var angleLeading:number = 5;

We'll need to add this to the rotation property:

tempText.rotation = radians2degrees(angle) -90 + angleLeading;

This gives us something like:

circular, rotated text with leading angle in flex

Ok, let's shift the starting point of our string and try to wrap this guy up.

As we've seen thus far, the x and y values are a function of the angular offset. If we want to shift the starting position of the string, what we really need to do is to offset the angle. I'm feeling another variable coming on. Oh yeah, here it is:

var angularOffset:Number = 90

And we just need to add this number into out x and y calculations:

tempText.x = radius * Math.cos(degrees2radians(char*spacing+angularOffset)) + center.x;
tempText.y = radius * Math.sin(degrees2radians(char*spacing+angularOffset)) + center.y;

So, our final version looks like this:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script>
<![CDATA[

import mx.components.TextView;
        
public var myText:String = "Read my blog @ http://blog.ShortFusion.com";
        
public function drawText():void {            
var radius:Number = 125;
var spacing:Number = 8;
var center:Point = new Point(125,125);
var angleLeading:Number = 5;
var angularOffset:Number = 180;
            
for (var char:uint=0;char<myText.length;char++) {
var tempText:TextView = new TextView();
tempText.x = radius * Math.cos(degrees2radians(char*spacing+angularOffset)) + center.x;
tempText.y = radius * Math.sin(degrees2radians(char*spacing+angularOffset)) + center.y;
var dx:Number = center.x-tempText.x;
var dy:Number = center.y-tempText.y;
var angle:Number = Math.atan2(dy, dx);
tempText.rotation = radians2degrees(angle) -90 + angleLeading;
tempText.text = myText.charAt(char);
textContainer.addChild(tempText);
}
}
        
private function degrees2radians(deg:Number):Number {
return (2 * Math.PI * deg) / 360;
}
        
private function radians2degrees(rad:Number):Number {            
return rad * 180 / Math.PI;
}
        
    ]]>

</mx:Script>
<mx:Button label="Draw a circle" click="drawText()" />
<mx:Canvas id="textContainer" y="50" x="50" width="400" height="400" />        
</mx:Application>

and we get the following:

circular, rotated text with leading angle and angular offset in flex

And you can go here to see a functioning demo of circular text in Flex.

Obviously there is a lot more one can do with this, but hopefully you got the gist of it.

[Edited 05MAR09 - I corrected random spelling issues and word omissions that made me wince as I reviewed it; sadly, I'm sure that there are more left to find.]

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
hasib's Gravatar hi boss, realy great example and good math.its help me very much.
is there any way to do it using displacementmapfilter,i need some text to bend & wrap dynamicaly that change with some given parameter?
thanks
# Posted By hasib | 8/18/09 8:12 AM
jason olmsted's Gravatar Actually, I did pursue the DisplacementMapFilter, and even blogged on it: http://blog.shortfusion.com/index.cfm/2008/12/15/F...
(a live link is a couple inches above this comment)

I wound up not going that route because of unattractive distortions to the text. I didn't like the end results so instead moved to the method described in this post.
# Posted By jason olmsted | 8/18/09 11:40 AM
Sweety's Gravatar Hi its really hep me..
thank a lot
:)
# Posted By Sweety | 11/25/09 1:49 AM
Sweety's Gravatar can u give me code fro draw rectangle and Triangle?

Thanks in advance..
# Posted By Sweety | 11/30/09 8:33 AM
abid's Gravatar its very good. & what is "import mx.components.TextView" how to found it
# Posted By abid | 1/5/10 6:06 AM
abid's Gravatar can u give me code for TextView class
# Posted By abid | 1/5/10 6:07 AM
abid's Gravatar can you give me a view text class
# Posted By abid | 1/5/10 8:03 AM
Angeline's Gravatar Hi, Everything looks great and the code is good, But I don't find the TextView class. It would be more helpful if you could post that code also
# Posted By Angeline | 1/8/10 6:47 AM
jason olmsted's Gravatar @Angeline & Abid:
So I thought I was going to get away with just making some flippant remark about google and reading documentation, but when I just tried to do that I got to share in your frustration - nuts.

As best as I can tell, there has been some naming shift in the SDK and what you likely need is TextDisplay or TextGraphics. My local developer environment uses a really old Flex 4 SDK, 4.0.0.4390, that I suppose I ought to update some time soon. The fun part of the beta process is that things are likely to change.

Hope that helps
# Posted By jason olmsted | 1/9/10 1:22 AM
Sweety's Gravatar Hi

You can use Lable instead of TextView. That Help u
# Posted By Sweety | 1/23/10 12:56 AM
ademus's Gravatar Thanx!
here's a similar script based upon some of your mathematics : http://www.softpeople.fr/Ressources/as3/curveText/...
# Posted By ademus | 7/20/10 3:47 PM
bhumi's Gravatar can u plzzz give me code of textview . I not found that so plzzzz help me
# Posted By bhumi | 12/16/10 2:25 AM
bhumi's Gravatar hi ademus thanx for this link..
and can u give me this same example in flex 3
plzzz
and if u have then send it to me on my id
bhumivaghani@yahoo.com
# Posted By bhumi | 12/16/10 7:29 AM
Aure's Gravatar You can help me so much, i hate the matematics!
Thanks for share
# Posted By Aure | 12/30/10 11:12 AM
amy flair's Gravatar Thanks It is that wat i want –
# Posted By amy flair | 2/2/11 12:23 AM
Woody's Gravatar Thanks so much for the tutorial.
Here is source with necessary
changes for Flash Builder 4.5.

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009";
          xmlns:s="library://ns.adobe.com/flex/spark"
          xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600">
   <fx:Declarations>
      <!-- Place non-visual elements (e.g., services, value objects) here -->
   </fx:Declarations>
   
   
   
   <fx:Script>
      <![CDATA[
         import mx.controls.Text;
         
         import spark.components.Label;
         
         public var myText:String = "Some more text for circle.";
         public function drawText():void {
            var radius:Number = 125;
            var spacing:Number = 8;
            var center:Point = new Point(125,125);
            var angleLeading:Number = 5;
            var angularOffset:Number = 180;
            for (var char:uint=0;char<myText.length;char++) {
               //var tempText:Text = new mx.controls.Text();
               var tempText:Label = new spark.components.Label();
               tempText.setStyle("fontSize", "18");
               tempText.x = radius * Math.cos(degrees2radians(char*spacing+angularOffset)) + center.x;
               tempText.y = radius * Math.sin(degrees2radians(char*spacing+angularOffset)) + center.y;
               var dx:Number = center.x-tempText.x;
               var dy:Number = center.y-tempText.y;
               var angle:Number = Math.atan2(dy, dx);
               tempText.rotation = radians2degrees(angle) -90 + angleLeading;
               tempText.text = myText.charAt(char);
               textContainer.addElement(tempText);
            }
         }
         private function degrees2radians(deg:Number):Number {
            return (2 * Math.PI * deg) / 360;
         }
         private function radians2degrees(rad:Number):Number {
            return rad * 180 / Math.PI;
         }
      ]]>
   </fx:Script>
   
   <s:Button label="Draw a circle" click="drawText()" />
   <s:Group id="textContainer" y="50" x="50" width="400" height="400" />
   
</s:Application>
# Posted By Woody | 8/12/11 2:59 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1.001. Contact Blog Owner