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.


No Comments Yet


There are no comments yet. You could be the first!

Leave a Comment

Speaking of tiny details OpenStreetMap vectors + Flash + Yahoo Maps