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;
}
}

2 comments:

  1. Do you have an example of this being used? I can't seem to get this work.

    ReplyDelete
  2. THX - very much!
    i use this in a pv3d twitter app and it worked aut of the box :)

    ReplyDelete