September 2, 2009

ActionScript 3: Papervision3d: html links in a TextField

Papervision is fun, and the most commonly used 3D library in Flash. The developers have done an amazing job taking care of so many issues that arise when rendering multiple interactive objects to one BitmapData object. For example, if you create a 2D Sprite, put a button inside it somewhere, and then use that Sprite as a MovieMaterial texture for a 3D object, the MouseEvent listeners are automatically handled, and your buttons in the texture work as expected, which is awesome. One major issue along these lines is that if you have an html-enabled TextField with active hyperlinks, this button functionality does not get forwarded through the Papervision core. I saw some code that another developer had posted on a forum, and I rewrote it to work nicely with multiple links. The idea is that you can find the character positions of a hyperlink's text in the TextField, and draw buttons as Sprites in the 2D texture, on top of the TextField, so that your hyperlinks will have an active hit state and work inside a 3D object. I also included a dispose function to clean up, and have rollover listeners to enable the hand cursor in the PV3D viewport, which happens in a different class. This all could've been done a little cleaner with multiple classes and more regular expressions, but I wanted to keep it really simple and easy to implement and garbage collect. Here's the code:

protected var _htmlButtons:Array;
protected var _htmlButtonLinks:Array;

/**
* Create hit areas for html links - since they aren't handled automatically by PV
*/
protected function activateHrefs( textField:TextField ):void
{
var htmlTxtStr:String = textField.htmlText;
var plainTxtStr:String = textField.text;
var linkOpens:Array = getIndexesOfArray( htmlTxtStr, "<a " );
var linkCloses:Array = getIndexesOfArray( htmlTxtStr, "</a>" );

_htmlButtons = new Array();
_htmlButtonLinks = new Array();

// helps step through and not repeat duplicate links
var lastPlanTextIndex:int = 0;

// loop through links found
for( var i:int = 0; i < linkOpens.length; i++ )
{
// create button
var button:Sprite = new Sprite();
button.x = textField.x;
button.y = textField.y;
this.addChild( button );

// get text position in html text
var firstCharIndex:int = linkOpens[i];
var linkLength:int = linkCloses[i] - linkOpens[i] + 4;

// pull out string inside open and close tags
var linkString:String = htmlTxtStr.substr( firstCharIndex, linkLength );

// get href from <a> tag
var hrefPattern:RegExp = /href=['"]\S+['"]/i;
var hrefs:Array = linkString.match( hrefPattern );
var href:String = ( hrefs ) ? hrefs[0].substring(6, hrefs[0].length - 1) : "";

// strip tags
linkString = linkString.substr( linkString.indexOf( ">" ) + 1 ); // chop open tag
linkString = linkString.substr( 0, linkString.length - 4 ); // chop end tag

// find link text in non-html text
var linkStringPlainTextIndex:int = plainTxtStr.indexOf( linkString, lastPlanTextIndex );
lastPlanTextIndex = linkStringPlainTextIndex;

// draw rects for letters
button.graphics.beginFill(0xFF0000, 0);
for( var j:int = linkStringPlainTextIndex; j < linkStringPlainTextIndex + linkString.length; j++ )
{
var charRect:Rectangle = textField.getCharBoundaries(j);
if( charRect ) button.graphics.drawRect(charRect.x, charRect.y, charRect.width, charRect.height);
}
button.graphics.endFill();

// add listeners
button.addEventListener( MouseEvent.CLICK, onHyperlinkClick );
button.addEventListener( MouseEvent.MOUSE_OVER, onHtmlLinkOver );
button.addEventListener( MouseEvent.MOUSE_OUT, onHtmlLinkOut );

// store button and link so we can launch on click
_htmlButtons.push( button );
_htmlButtonLinks.push( href );
}
}

/**
* Returns an array of all the indexes of needle in haystack
*/
protected function getIndexesOfArray( haystack:String, needle:String ) : Array
{
var indexs:Array = new Array();
var startIndex:int = 0;
while( startIndex != -1 )
{
startIndex = haystack.indexOf( needle, startIndex );
if( startIndex != -1 )
{
indexs.push( startIndex );
startIndex += 1;
}
}
return indexs;
}

/**
* simply opens the link
*/
protected function onHyperlinkClick( e:MouseEvent ) : void
{
// find button and launch corresponding link
for( var i:int = 0; i < _htmlButtons.length; i++ )
{
if( e.target == _htmlButtons[i] )
{
navigateToURL( new URLRequest( _htmlButtonLinks[i] ), '_blank' );
}
}
}

protected function onHtmlLinkOver( e:MouseEvent ):void
{
// dispatch an Event to tell the PV3D viewport to enable the hand cursor:
// ( _pvView as BasicView).viewport.buttonMode = true;
}

protected function onHtmlLinkOut( e:MouseEvent ):void
{
// dispatch an Event to tell the PV3D viewport to disable the hand cursor:
// ( _pvView as BasicView).viewport.buttonMode = false;
}

/**
* clean up when if leave the papervision section
*/
public function dispose():void
{
// kill html hyperlink buttons
if(_htmlButtons != null) {
for( var i:int = 0; i < _htmlButtons.length; i++ )
{
_htmlButtons[i].removeEventListener( MouseEvent.CLICK, onHyperlinkClick );
_htmlButtons[i].removeEventListener( MouseEvent.MOUSE_OVER, onHtmlLinkOver );
_htmlButtons[i].removeEventListener( MouseEvent.MOUSE_OUT, onHtmlLinkOut );
}
_htmlButtons.splice( 0 );
_htmlButtonLinks.splice(0);
_htmlButtons = null;
_htmlButtonLinks = null;
}
}

September 1, 2009

ActionScript 3: Choose a random item from an Array, with weighting

It's very common to simply choose a random item from an Array, using a random number. But what if we want change the probabilities that certain items will be chosen? I've written a little function that takes an Array of weights (Numbers), which correspond to the Array of items you'd like to choose from, and returns an index to pull a random, weighted item from the source Array. Check out the example:

// our array of items
var fruits:Array = ['apple','orange','banana','mango'];
// our array of weights
var weights:Array = [20,10,40,30];
// pick a random fruit, based on weights, with bananas most likely to get picked
var myFruit:String = fruits[ randomIndexByWeights( weights ) ];

/**
* Takes an array of weights, and returns a random index based on the weights
*/
private function randomIndexByWeights( weights:Array ) : int
{
// add weights
var weightsTotal:Number = 0;
for( var i:int = 0; i < weights.length; i++ ) weightsTotal += weights[i];
// pick a random number in the total range
var rand:Number = Math.random() * weightsTotal;
// step through array to find where that would be
weightsTotal = 0;
for( i = 0; i < weights.length; i++ )
{
weightsTotal += weights[i];
if( rand < weightsTotal ) return i;
}
// if random num is exactly = weightsTotal
return weights.length - 1;
}

You can see that the weights array must be the same length as the data array that you're choosing from. Note that in the example, my weights add up to 100, but you can use any scale that you'd like, as the weights are added up, and a random number is chosen in that scale.

ActionScript 3: Shuffle/randomize any Array

A quick little function for randomizing/shuffling an Array that contains objects or primitive of any data type:

public static function randomizeArray( arr:Array ):void
{
for( var i:int = 0; i < arr.length; i++ )
{
var tmp:* = arr[i];
var randomIndex:int = Math.round( Math.random() * ( arr.length - 1 ) );
arr[i] = arr[randomIndex];
arr[randomIndex] = tmp;
}
}

You can see that it crawls through an Array, swapping each position with another, random position in the Array.

ActionScript 3: Adding a textual suffix to numbers

If you need to write out a number with a textual suffix, like "25th" or "173rd", there's a little logic that will make this really easy. Check it out:

// suffixes corresponding to the last digit of a number: 0-9
private static const NUMBER_SUFFIXES:Array = [ 'th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th' ];

private function getNumberSuffix( value : int ) : String
{
// handle most cases by modding by ten
var suffix:String = NUMBER_SUFFIXES[ value % 10 ];
if( value % 100 >= 11 && value % 100 <= 13 ) suffix = 'th'; // handle 11-13
if( value == 0 ) suffix = ''; // handle zero
return suffix;
}