Random Etc. Notes to self. Work, play, and the rest.

Text along a path in Flash

Trevor McCauley did the hard part years ago, as is often the case, but it seems like there's no source code out there showing text along a path in Flash using actionscript 3. I'm not the only one thinking about this – the degrafa folks have got the extremely capable algorithmist Jim Armstrong looking into the problem too.

I don't have time to write a full explanation tonight (packing calls, tomorrow I'm in Montréal for Design Engaged), but I've got a quick solution which might be of use to you if you googled upon this page. Read on for more if you're interested in a quick overview.

Here's a test showing two stars, one created from the outside-in and one created from the inside-out.

The source code:

 
 
package {
  import com.senocular.drawing.Path;
 
  import flash.display.Sprite;
  import flash.filters.GlowFilter;
  import flash.geom.Point;
  import flash.text.TextFormat;
  import flash.text.TextFormatAlign;
 
  [SWF(backgroundColor='#ffffff')]
  public class PathTests extends Sprite
  {
     [Embed(systemFont="Helvetica Neue", fontName="Helvetica", mimeType='application/x-font', unicodeRange='U+00A0,U+0020-U+007E,U+00A1-U+00BF,U+02BB-U+02BC,U+2010-U+2015,U+2018-U+201D,U+2024-U+2026')]
    private static var _ignoreMe:String;
 
    [Embed(systemFont="Helvetica Neue", fontName="Helvetica", fontWeight="bold", mimeType='application/x-font', unicodeRange='U+00A0,U+0020-U+007E,U+00A1-U+00BF,U+02BB-U+02BC,U+2010-U+2015,U+2018-U+201D,U+2024-U+2026')]
    private static var _ignoreMeToo:String;
 
    public function PathTests()
    {
      stage.align = 'TL';
      stage.scaleMode = 'noScale';
 
      drawStar(new Point(stage.stageWidth/4, stage.stageHeight/2), true);
      drawStar(new Point(3*stage.stageWidth/4, stage.stageHeight/2), false);
    }
 
    public function drawStar(center:Point, inToOut:Boolean):void
    {
      var textFormat:TextFormat = new TextFormat("Helvetica", 11, 0x000000, true);
      textFormat.kerning = true;
      textFormat.letterSpacing = 0;
      textFormat.align = TextFormatAlign.CENTER;
 
      var innerRadius:Number = 50;
      var outerRadius:Number = 125;
      var midRadius:Number = (innerRadius + (outerRadius-innerRadius)/2); 
 
      var path:Path;
 
      var paths:Array = [];
 
      for (var a:Number = 0; a < 2*Math.PI; a+=Math.PI/18) {
        path = new Path();
        paths.push(path);
 
        if (inToOut) {
          path.moveTo(center.x + innerRadius * Math.cos(a),
                      center.y + innerRadius * Math.sin(a));
          path.lineTo(center.x + midRadius * Math.cos(a - Math.PI/36),
                      center.y + midRadius * Math.sin(a - Math.PI/36));
          path.lineTo(center.x + outerRadius * Math.cos(a),
                      center.y + outerRadius * Math.sin(a));
        }
        else {
          path.moveTo(center.x + outerRadius * Math.cos(a),
                      center.y + outerRadius * Math.sin(a));
          path.lineTo(center.x + midRadius * Math.cos(a + Math.PI/36),
                      center.y + midRadius * Math.sin(a + Math.PI/36));
          path.lineTo(center.x + innerRadius * Math.cos(a),
                      center.y + innerRadius * Math.sin(a));
        }
 
        var pathField:TextPathField = new TextPathField('Sample text', path, textFormat);
        pathField.filters = [ new GlowFilter(0xffffff, 1, 2, 2, 4, 1, false, false) ]
        addChild(pathField);
      }
 
      graphics.lineStyle(13,0xff9900);
      for each (path in paths) {
        path.draw(graphics);
      }
      graphics.lineStyle(11,0xffff00);
      for each (path in paths) {
        path.draw(graphics);
      }
    }
  }
}
 

And the TextPathField code that powers it:

 
 
package
{
  import com.senocular.drawing.Path;
 
  import flash.display.Sprite;
  import flash.geom.Matrix;
  import flash.geom.Point;
  import flash.geom.Rectangle;
  import flash.text.AntiAliasType;
  import flash.text.GridFitType;
  import flash.text.TextField;
  import flash.text.TextFormat;
  import flash.text.TextFormatAlign;
 
  /** depends on com.senocular.drawing.Path */
  public class TextPathField extends Sprite
  {
    public static const DEFAULT_TEXTFIELD_PARAMS:Object = {
      embedFonts: true,
      antiAliasType: AntiAliasType.ADVANCED,
      gridFitType: GridFitType.NONE
    };
 
    // TODO: getters and setters that call redraw
    public var text:String;
    public var path:Path;
 
    public var textFormat:TextFormat;
    public var textFieldParams:Object;
 
    public function TextPathField(text:String, path:Path, textFormat:TextFormat, textFieldParams:Object=null)
    {
      mouseEnabled = false;
      mouseChildren = false;
 
      this.text = text;
      this.path = path;
      this.textFormat = textFormat;
      this.textFieldParams = textFieldParams || DEFAULT_TEXTFIELD_PARAMS;
 
      redraw();
    }
 
    public function redraw():void
    {
      // clear everything away...
      while (numChildren > 0) {
        removeChildAt(0);
      }
 
      // make a dummy textfield so we can measure its width
      var textField:TextField = new TextField();
      applyParamsToField(textField);
      textField.defaultTextFormat = textFormat;
      textField.text = text;
      textField.width = textField.textWidth+4;
      textField.height = textField.textHeight+4;
 
      // bail out if there's no room
      if (path.length < textField.width) return;
 
      var t1:Number;
      var t2:Number;
 
      if (textFormat.align == TextFormatAlign.LEFT || textFormat.align == null) {
        t1 = 0;
        t2 = textField.width / path.length;
      }
      else if (textFormat.align == TextFormatAlign.RIGHT) {
        t1 = 1.0 - (textField.width / path.length);
        t2 = 1.0;
      }
      else if (textFormat.align == TextFormatAlign.CENTER) {
        t1 = (path.length/2 - textField.width/2) / path.length;
        t2 = (path.length/2 + textField.width/2) / path.length;
      }
      else {
        // throw error?
        trace('justify alignment unsupported in TextPathField');
      }
 
      var angleOffset:Number; // so we can do a 180º if we're running backwards
      var offsetSign:Number;  // -1 if we're starting at t2
      var tStart:Number;      // t1 or t2
 
      // TODO: there are probably more than two cases here...?
       if (path.pointAt(t1).x < path.pointAt(t2).x && path.angleAt(t1) < Math.PI/2 && path.angleAt(t1) > -Math.PI/2) {
        angleOffset = 0;
        offsetSign = 1;
        tStart = t1;
      }
      else {
        // this catches text that's running right to left or upside down
        angleOffset = Math.PI;
        offsetSign = -1;
        tStart = t2;
      } 
 
      // make a textfield for each char, centered on the line, using getCharBoundaries to rotate it around its center point
      var chars:Array = text.split('');
      for (var i:int = 0; i < chars.length; i++) {
        var rect:Rectangle = textField.getCharBoundaries(i);
        var yOffset:Number = textField.height/2;
        if (rect) {
          var t:Number = tStart + offsetSign*(rect.left+rect.width/2)/path.length;
          addCharTextField(chars[i], path.pointAt(t), new Point(rect.width/2, yOffset), angleOffset+path.angleAt(t));
        }
      }
    }
 
    // place the given character at pt, registered using rpt, and rotated by r radians
    private function addCharTextField(char:String, pt:Point, rpt:Point, r:Number):void
    {
      var textField:TextField = new TextField();
      applyParamsToField(textField);
      textField.defaultTextFormat = textFormat;
      textField.text = char;
      textField.width = textField.textWidth+4;
      textField.height = textField.textHeight+4;
 
      var matrix:Matrix = new Matrix();
      matrix.translate(-rpt.x, -rpt.y);
      matrix.rotate(r);
      //matrix.translate(rpt.x, rpt.y);
      matrix.translate(pt.x, pt.y);
      textField.transform.matrix = matrix;
 
      addChild(textField);
    }
 
 
    private function applyParamsToField(textField:TextField):void
    {
      for (var param:String in textFieldParams) {
        if (textField[param]) {
          textField[param] = textFieldParams[param];
        }
      }
    }
 
  }
 
}
 

Hopefully I can post more about how I'm using this class soon.


7 Comments

Does it have to be Helvetica font? Will another font work? I don’t have Helvetica installed and have tried using Arial and a few other fonts without success. Do I have to do anything with CSS?

Posted by Michael Deutch on 29 January 2009 @ 12pm

Hi Michael

It can be any font, but it has to be embedded otherwise the rotation won’t work.

Font embedding can be tricky to get your head around, so depending on whether you’re using the Flash IDE or Flex Builder or another compiler, you’ll have to look at the font embedding techniques for your chosen environment.

Posted by TomC on 29 January 2009 @ 6pm

Hi, great work really, i want to ask that is it possible to arc or bulge text using matrix

Posted by Awais on 19 March 2009 @ 6am

Hi there!

There seems to be a bug, I’m surprised it’s working for anybody else.

In applyParamsToField, the embedFonts property is NOT being set up for the text field because the the if returns false. Change it to:

if (textField[param] != null)

And it works a treat. Thanks very much!

Posted by Zarate on 27 May 2009 @ 6am

Thanks Zarate. I’m not sure if this code will work with non-embedded fonts though - does it?

Nevertheless, it’s a good catch. My code was sloppy. Now I’d write:

if (param in textField)

Which is what we really mean.

Thanks again!

Posted by TomC on 27 May 2009 @ 11am

Hi again,

Yeah, this won’t work without embdedding the font. I have a couple of comments:

- In your class, when you bail out if there’s no room, it might be better throwing an exception, otherwise sometimes you see nothing but don’t know why. Or maybe a simple event? Like NOT_ENOUGH_ROOM or something like that?

- Why don’t you put the class in your own package? I think you totally deserve the recognition to your work! And while you are at it, license it adding any of the OSI licensese at the very top(BSD or MIT recommended):

http://www.opensource.org/licenses/alphabetical

Thanks again for your work, saved me countless hours : )

Posted by Zarate on 29 May 2009 @ 2am

Thanks this is helfull, but i not used like i just used the text creator, and used “rotateZ” a not “rotate” to rotate a text… I have found a usefull links at: http://www.yswfblog.com/blog/2009/05/21/the-knack-to-rotating-dynamic-text-in-flash-10/comment-page-1/#comment-79198

Posted by Eduardo Sandino on 5 June 2009 @ 2pm

Leave a Comment

Speaking of tiny details OpenStreetMap vectors + Flash + Yahoo Maps