Textured text in AS3

Ever feel the desire to see your text in anything other than the default dull-as-muck solid colours? I sure do! Which is why I spent a few hours creating this TexturedText class.

Relive all the cool effects from the 80's and impress your friends. In fact, you'll probably win some friends if you use this in your projects. Some of the features:

  • It's based on Bitmap so it can do all the cool things Bitmap can do. Like...um, pixel snapping and stuff.
  • It'll use any BitmapData you want for the texture, or if you like plagiarising stuff, pass a String URL for it to load that image.
  • It'll work with any font you want (big fonts look better). Embed your fonts, use TextFormats to make it bigger and stuff.
  • If you use a texture that's too small, it'll draw your text in patches. Amazing!
  • Use all your favourite TextField properties, like text, textFormat, embedFonts, multiline and wordWrap.
  • Change text/texture on the fly!
  • Cleans up after itself when you want to abandon it.

On to the code:

package  
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Loader;
	import flash.display.LoaderInfo;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.net.URLRequest;
	import flash.system.LoaderContext;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;

	/**
	 * Creates a TextField that's textured by an image
	 */
	public class TexturedTextField extends Bitmap
	{
		/**************************************************************************************************/
		
		/**
		 * When using the texture, should we sample a rample position?
		 */
		public var useRandomTexturePos:Boolean = true;
		
		/**************************************************************************************************/

		private var m_text:TextField		= null;         // the TextField that we use to write our text
		private var m_texture:BitmapData	= null;         // the texture that we're going to texture our TextField with
		private var m_textBMD:BitmapData	= null;         // the BitmapData that we use to draw our TextField
		private var m_drawPoint:Point		= new Point;    // Point used in drawing the final BitmapData
		private var m_drawRect:Rectangle	= new Rectangle;// the Rectangle we use to determine which part of the texture to take
		private var m_loader:Loader			= null;			// the loader object we use to load in our image
		private var m_context:LoaderContext	= null;			// the loader context for our loader object

		/**************************************************************************************************/
		
		/**
		 * The text to display
		 */
		public function get text():String { return this.m_text.text; }
		public function set text( s:String ):void
		{
			this.m_text.text = s;
			this._redraw();
		}
		
		/**
		 * The TextFormat to use when rendering the text
		 */
		public function set textFormat( tf:TextFormat ):void
		{
			this.m_text.defaultTextFormat 	= tf;
			this.m_text.text				= this.m_text.text; // reset the text so the textformat takes effect
			this._redraw();
		}
		
		/**
		 * Is the font we're using embedded?
		 */
		public function set embedFonts( b:Boolean ):void
		{
			this.m_text.embedFonts = b;
			this._redraw();
		}
		
		/**
		 * If the TextField is a multiline TextField
		 */
		public function set multiline( b:Boolean ):void
		{
			this.m_text.multiline = b;
		}
		
		/**
		 * If the text is too long for the TextField, should it wrap?
		 */
		public function set wordWrap( b:Boolean ):void
		{
			this.m_text.wordWrap = b;
			this._redraw();
		}
		
		/**
		 * The width of the TextField in pixels
		 */
		override public function get width():Number { return this.m_text.width; }
		override public function set width( n:Number ):void
		{
			this.m_text.width = n;
			this._redraw();
		}
		
		/**
		 * The height of the TextField in pixels
		 */
		override public function get height():Number { return this.m_text.height; }
		override public function set height( n:Number ):void
		{
			this.m_text.height = n;
			this._redraw();
		}
		
		/**
		 * The texture that we want to use for this TextField. Can be either a BitmapData
		 * or a URL to load our texture from. NOTE: This won't call dispose() on any existing
		 * texture, so you'll need to clean it up yourself
		 */
		public function set texture( t:* ):void
		{			
			// if it's not a BitmapData or a String, don't do anything
			if ( t != null && !( t is BitmapData ) && !( t is String ) )
			{
				trace( "3:The texture object passed (" + t + ") isn't a BitmapData object or a URL" );
				return;
			}
			
			// if we already have a texture, kill it
			if ( this.m_texture != null )
				this.m_texture = null;
			
			// if it's null, do nothing
			if ( t == null )
				return;
			
			// if we already have a loader, stop it
			if ( this.m_loader != null )
				this._killLoader();

			// if it's a BitmapData object, just store it directly
			if ( t is BitmapData )
			{
				this.m_texture = t;
				this._redraw(); // redraw immediately (the loading will call redraw when it's done)
			}
			else
			{
				// create our loader and listeners
				this.m_loader = new Loader;
				this.m_loader.contentLoaderInfo.addEventListener( Event.COMPLETE, this._onTextureLoad );
				this.m_loader.contentLoaderInfo.addEventListener( IOErrorEvent.IO_ERROR, this._onIOError );
				this.m_loader.contentLoaderInfo.addEventListener( SecurityErrorEvent.SECURITY_ERROR, this._onSecurityError );
				
				// create our context if needed
				if ( this.m_context == null )
					this.m_context = new LoaderContext( true );

				// load the image
				try
				{
					this.m_loader.load( new URLRequest( t ), this.m_context );
				}
				catch ( e:SecurityError )
				{
					trace( "3:A Security error occured: " + e.errorID + ": " + e.message );
				}
			}
		}

		/**************************************************************************************************/

		/**
		 * Creates a new TexturedTextField
		 * @param texture The texture we want to use; either a BitmapData, or a url to load
		 */
		public function TexturedTextField( texture:* = null ) 
		{
			// create our textfield
			this.m_text				= new TextField;
			this.m_text.autoSize	= TextFieldAutoSize.LEFT;

			// set our texture
			this.texture = texture;
		}
		
		/**
		 * Destroys the TexturedTextField and clears it for garbage collection
		 * @param killTexture Should we call dispose() on the texture BitmapData object?
		 */
		public function destroy( killTexture:Boolean ):void
		{
			// kill the loader
			this._killLoader();
			
			// dispose of the BitmapDatas so we save memory immediately
			if ( killTexture )
				this.m_texture.dispose();
			this.m_textBMD.dispose();
			this.bitmapData.dispose();
			
			// null our objects
			this.m_text 		= null;
			this.m_texture		= null;
			this.m_textBMD		= null;
			this.m_drawPoint	= null;
			this.m_drawRect		= null;
			this.m_loader		= null;
		}

		/**************************************************************************************************/

		// redraws the text so any changes take place
		private function _redraw():void
		{
			// if we've no texture, just return
			if ( this.m_texture == null )
				return;
				
			// if our textfield is empty, just return
			if ( this.m_text.text == "" )
			{
				if ( this.bitmapData != null )
					this.bitmapData.fillRect( this.bitmapData.rect, 0x00000000 );
				return;
			}

			// get the width and height of our text
			var tw:int	= int( this.m_text.width + 0.5 ); // quick convert to int without clipping
			var th:int	= int( this.m_text.height + 0.5 );

			// reuse our previous BitmapData if we can, rather than always creating a new one
			if ( this.m_textBMD == null || this.m_textBMD.width < tw || this.m_textBMD.height < th )
			{
				// dispose immediately to save memory
				if ( this.m_textBMD != null )
					this.m_textBMD.dispose();
				this.m_textBMD = new BitmapData( tw, th, true, 0x00000000 );
			}
			else
				this.m_textBMD.fillRect( this.m_textBMD.rect, 0x00000000 ); // clear the bitmapdata of the old rendering

			// draw our text
			this.m_textBMD.draw( this.m_text, null, null, null, null, true );

			// set our draw rect position
			this.m_drawRect.x		= ( this.useRandomTexturePos ) ? Math.random() * ( this.m_texture.width - tw ) : 0.0;
			this.m_drawRect.y		= ( this.useRandomTexturePos ) ? Math.random() * ( this.m_texture.height - tw ) : 0.0;
			this.m_drawRect.width	= ( tw < this.m_texture.width ) ? tw : this.m_texture.width;
			this.m_drawRect.height	= ( th < this.m_texture.height ) ? th : this.m_texture.height;
			
			// make sure the draw rect x and y aren't minus
			if ( this.m_drawRect.x < 0.0 ) this.m_drawRect.x = 0.0;
			if ( this.m_drawRect.y < 0.0 ) this.m_drawRect.y = 0.0;
			
			// reset the draw point position
			this.m_drawPoint.x = 0.0;
			this.m_drawPoint.y = 0.0;

			// dispose of the previous bitmap if there is one to save memory
			if ( this.bitmapData != null )
				this.bitmapData.dispose();
				
			// create our bitmapdata and copy our pixels, using the text bmd as an alpha mask
			var doY:Boolean	= false; // do we need to move the y as well?
			this.bitmapData = new BitmapData( tw, th, true, 0xf7000000 );
			this.bitmapData.lock();
			while ( true )
			{
				// copy the pixels over
				this.bitmapData.copyPixels( this.m_texture, this.m_drawRect, this.m_drawPoint, this.m_textBMD, this.m_drawPoint );
				
				// if this needs to be done in multiple segments, it'll travel along the x,
				// then move down a bit and travel along the x again, until it's done
				
				// if our texture wasn't big enough, we need to do this in a few turns.
				// check do we need to do the y
				doY = ( this.m_drawPoint.y + this.m_drawRect.height < th );
				
				// do we need to do the x?
				if ( this.m_drawPoint.x + this.m_drawRect.width < tw )
					this.m_drawPoint.x += this.m_drawRect.width; // move along
				else if ( doY )
				{
					this.m_drawPoint.x = 0.0; // reset the x
					this.m_drawPoint.y += this.m_drawRect.height;
					doY = false;
				}
				else
					break; // it's fine, we've finished
			}
			
			this.bitmapData.unlock();
		}

		// called when our texture is loaded
		private function _onTextureLoad( e:Event ):void
		{
			// get the loader info and extract the bitmap from it
			var info:LoaderInfo = e.target as LoaderInfo;
			try
			{
				var bitmap:Bitmap = info.content as Bitmap; // accessing this will throw an error if we don't
															// have permission
			}
			catch ( err:SecurityError )
			{
				trace( "3:A Security error occured: " + err.errorID + ": " + err.message );
			}
			
			// if our bitmap is null, return
			if ( bitmap == null )
				return;

			// store our texture and clear the loader
			this.m_texture 		= bitmap.bitmapData;
			bitmap.bitmapData	= null;
			this._cleanUp( info );
			
			// redraw the text
			this._redraw();
		}

		// called when there's been an io error when loading our texture
		private function _onIOError( e:IOErrorEvent ):void
		{
			trace( "3:There was an io error: errorID: " + e.errorID + ", text: " + e.text );
			this._cleanUp( e.target as LoaderInfo );
		}

		// called when there's been a security error when loading our texture
		private function _onSecurityError( e:SecurityErrorEvent ):void
		{
			trace( "3:There was a security error: errorID: " + e.errorID + ", text: " + e.text );
			this._cleanUp( e.target as LoaderInfo );
		}

		// clean up the listeners for the texture loader
		private function _cleanUp( info:LoaderInfo ):void
		{
			info.addEventListener( Event.COMPLETE, this._onTextureLoad );
			info.addEventListener( IOErrorEvent.IO_ERROR, this._onIOError );
			info.addEventListener( SecurityErrorEvent.SECURITY_ERROR, this._onSecurityError );
			
			// clear our loader
			this.m_loader = null;
		}
		
		// kills the loader if there's a load in progress
		private function _killLoader():void
		{
			if ( this.m_loader == null )
				return;
			
			// close the loader and clean up
			this.m_loader.close();
			this._cleanUp( this.m_loader.contentLoaderInfo ); // removes the event listeners
		}

	}

}

So how do you actually use this in a project? It couldn't be easier:

var ttf:TexturedTextField = new TexturedTextField( texture ); // texture is a BitmapData, but it could also be a URL string
ttf.textFormat	= new TextFormat( "Trebuchet Ms", 50.0, null, true );
ttf.text 		= "Hello world";
this.addChild( ttf );

Incredible! Play around with it in the SWF below, and get the code if you want.

Share: