Archive for the ‘AS3’ Category

Insert image from File System into text using the Text Layout Framework

Monday, March 23rd, 2009

For the past few weeks I've been building a Rich Text Editor using the TLF. It's been a great opportunity to dig into it, and I have to say it's mostly been a pleasure. One of the features was to insert images, which turned out to be easy. So dead easy, that I found it cool enough to warrant a blog post.

Click the image for a demo:

Once you've created a TextFlow and activated an EditManager (Making the text editable), it takes just 4 steps:

1) Open an OS Dialogue box, wait for the user to select an image file:

Actionscript:
  1. public function addImage(e:MouseEvent):void {
  2.   _file_ref = new FileReference();
  3.   _file_ref.addEventListener( Event.SELECT, handleFileSelect,false,0,true );
  4.    var filter:FileFilter = new FileFilter("Images", "*.jpg;*.gif;*.png");
  5.   _file_ref.browse([filter]);
  6. }

2) Load the selected file:

Actionscript:
  1. protected function handleFileSelect(e:Event):void {
  2.   _file_ref.removeEventListener( Event.SELECT, handleFileSelect );
  3.   _file_ref.addEventListener(Event.COMPLETE, handleFileOpen,false,0,true );
  4.   _file_ref.load();
  5. }

3) The file is now in the form of a ByteArray. Use a Loader, to loadBytes() and wait for a COMPLETE event.

Actionscript:
  1. protected function handleFileOpen(e:Event):void {
  2.   _file_ref.removeEventListener(Event.COMPLETE, handleFileOpen );
  3.   var data:ByteArray = _file_ref.data as ByteArray;
  4.   _img_loader=new Loader();
  5.   _img_loader.loadBytes(data);
  6.   _img_loader.contentLoaderInfo.addEventListener(Event.COMPLETE,imageLoadComplete,false,0,true);
  7. }

4) Then "copy" the BitmapData into a new Bitmap. This might seem a bit cryptic, a good explanation can be found in this thead Finally, the "one liner" or the "punchline" of this blog post: use EditManager.insertInlineGraphic() to plug it into the textFlow.

Actionscript:
  1. protected function imageLoadComplete(e:Event):void{
  2.   _img_loader.contentLoaderInfo.removeEventListener(Event.COMPLETE,imageLoadComplete);
  3.   var bmd:BitmapData=Bitmap(_img_loader.content).bitmapData;
  4.   var bm:Bitmap=new Bitmap(bmd);
  5.   EditManager(_flow.interactionManager).insertInlineGraphic(bm,bm.width,bm.height);
  6.   _flow.flowComposer.updateAllContainers();
  7. }

That is all... Well, for this to be useful, the image will have to be stored somewhere, if the document being created is to be saved or sent somewhere.

download the source (just one zipped mxml file)

For further reading, here's a couple useful links concerning the TLF:

How to use the TLF tutorial
TLF online forum
online TLF documentation

Simple Shape Extruder using FP10 Drawing API

Tuesday, March 10th, 2009

My SoTexty 3d Text Effect demo uses a Class I wrote a which takes a polygon in the form of a Vector of Points, and extrudes it into a 3dMesh of sorts.

Click the image for a demo:

The colors are random, as is the outline of the shapes... Refresh if the result doesn't please your aesthetics ;)

Here's the snippet that uses the class:

Actionscript:
  1. protected function testExtrude():void{
  2.     var points:Vector.<Point>=createCircularPath(new Point(300,300),200,250,50);
  3.     var negative_shape_points:Vector.<Point>=createCircularPath(new Point(300,300),90,190,40);
  4.     var points2:Vector.<Point>=createCircularPath(new Point(550,550),80,20,20);
  5.     var points3:Vector.<Point>=createCircularPath(new Point(300,650),80,20,20);
  6.     var points4:Vector.<Point>=createCircularPath(new Point(650,300),80,20,20);
  7.     _extruded=SimpleShapeExtruderDrawAPI.createExtrudedShapeFromPoints(100,
  8.                                                                 Vector.<Vector.<Point>>([points,points2,points3,points4]),
  9.                                                                 Vector.<Vector.<Point>>([negative_shape_points]),
  10.                                                                 Math.random()*0xFFFFFF,0xFF000000+Math.random()*0xFFFFFF
  11.                                                                 );
  12.     _extruded.adjustCenter(300,300);
  13.     _extruded.x=300;
  14.     _extruded.y=300;
  15.     addChild(_extruded);
  16.     addEventListener(Event.ENTER_FRAME,rotateExtruded);
  17. }

Instead of Triangulating the shape, I used a "shortcut" which I learned a while back from Den Ivanov. The Mesh contains a "front" and a "back" which are a triangle pair, who use the "original" shape as their material:

The large red square with the diagonal represents the "front", the "back" is identical. A curiosity about this screenshot is that the "filled triangles" are z-sorted correctly, however, there are some issues with the outlines. Meh.

The code accepts a Vector of Positive and Negative "shapes", which are rendered into a BitmapData and used as a "Material" by the 3DMesh. Here is a screenshot of what the "rendered" material looks like:

The filled box on the right is the "material" used by the extruded sides.

Notice the white space surrounding the shape. An annoying issue I ran into was that the "front" and the "back" had unwanted artifacts when using a "1 to 1" ratio "material". The UV mapping would go from 0 to 50% horizontally, and 0 to 100% vertically. For some reason, the edges (when rotated) would render an arbitrary "line" on any of the four sides.

The solution was to add white space, and grab the material using "less precise" percentages, which unfortunately results in some "spaces" on the edges of the "shape" material and the extruded side triangles. Another Meh.

It's not perfect, but I figured the code might be fun to play with...

Download the flex project here

(requires Flex SDK 3.2 or higher)

Extracting positive and negative shapes from a BitmapData

Tuesday, March 3rd, 2009

The first step in generating the "vector text effects" which I blogged about in my previous post is to grab a BitmapData of a character, then seperate the "characters components" into individual BitmapData instances.

So let's take a capital B :

The B only contains one "positive shape", that is, the "filled in" part. The B contains 3 "negative shapes" or the transparent bits. In this case we've got the surrounding area and the two "holes".

If we take a percent symbol "%"

We've got 3 positive shapes and 3 negative ones.

So how does one go about extracting these? I'm sure there are several approaches, I tried a few, this was the speediest of the ones I came up with.

Step 1 : Convert the captured BitmapData into one containing just two colors, a solid and a transparent. *Note:* this has the side effect that the extracted shapes have "pixelated" , "jagged" or "aliased" edges. For my purposes this is sufficient, a different approach would be needed to keep anti-aliasing.

Actionscript:
  1. public static function toMonoChrome(source:BitmapData,mono_color:uint=0xFF000000):BitmapData{
  2.     var bmd:BitmapData=source.clone();
  3.     bmd.threshold(bmd,bmd.rect,new Point(),">",0x00000000,mono_color);
  4.     return bmd;
  5. }

Step 2 : Create an "inverse" of the "monochrome" image:

Actionscript:
  1. //create the monochrome for extracting "positive shapes"
  2. var positive_two_color:BitmapData=new BitmapData(source_bmd.width,source_bmd.height,true,0x00);
  3. positive_two_color.draw(source_bmd);
  4. positive_two_color=BitmapDataUtil.toMonoChrome(positive_two_color,0xFFFF0000);
  5.  
  6. //create the monochrome for extracting "negative shapes"
  7. var temp:BitmapData=new BitmapData(source_bmd.width,source_bmd.height,true,0xFF0000FF);
  8. temp.draw(positive_two_color);
  9. var negative_two_color:BitmapData=new BitmapData(source_bmd.width,source_bmd.height,true,0xFFFF0000);
  10. negative_two_color.copyChannel(temp,positive_two_color.rect,new Point(), BitmapDataChannel.BLUE, BitmapDataChannel.ALPHA);

Again, I'm sure there are other ways to skin this cat. I created a BLUE "temp" BitmapData, then drew the "positive shape" on it. I then copy the BLUE Channel into the ALPHA channel of a RED BitmapData, which creates an image with inverted RED and ALPHA from the "positive shape".

Step 3 : Extract the "component" BitmapDatas from the positive and negative BitmapDatas. This requires a few steps:

Create a loop which:

  1. finds the first non transparent pixel more info
  2. Does a FloodFill with Blue on that pixel
  3. Create a new BitmapData, and copy the BLUE Channel into it, add this to the "found" items
  4. Remove the Blue from the original by using a transparent FloodFill

Do this for both the "positive" and inverse "negative" BitmapDatas and store the discovered "component shapes" in a vector or an array.

Here's my code for doing so:

Actionscript:
  1. protected static function extractShapesFromMonochrome(bmd:BitmapData):Vector.<BitmapData>{
  2.     var scan_y:uint=0;
  3.     var non_trans:Point;
  4.     var found:Vector.<BitmapData>=new Vector.<BitmapData>();
  5.     var copy_bmd:BitmapData;
  6.     while(scan_y<bmd.height){
  7.         non_trans=BitmapDataUtil.getFirstNonTransparentPixel(bmd,scan_y);
  8.         if(non_trans==null)return found;
  9.         bmd.floodFill(non_trans.x,non_trans.y,0xFF0000FF);//fill with blue
  10.         copy_bmd=new BitmapData(bmd.width,bmd.height,true,0xFFFF0000);
  11.         copy_bmd.copyChannel(bmd,bmd.rect,new Point(),BitmapDataChannel.BLUE , BitmapDataChannel.ALPHA);
  12.         bmd.floodFill(non_trans.x,non_trans.y,0x00000000);//fill with blue
  13.         found.push(copy_bmd);
  14.         scan_y=non_trans.y;
  15.     }
  16.     return found;
  17. }

That pretty much does it. The next step is to analyze the discovered shapes and generate vector points to work with... but that's another blog post.

Get first non transparent pixel in a bitmap Part III

Wednesday, February 18th, 2009

I know, I know... such a riveting topic, just can't get enough! :D

So I felt quite embarrassed when Mario commented on my previous entry, stating that in fact I had misunderstood his method. Either just me being an idiot, or me being an idiot after a few drinks at the Max party.

He then proceeded to blog about the real deal. Indeed this requires less code, looks cleaner and more 1337 in every way! I then included the new method into my test run.

Here is Marios code:

Actionscript:
  1. public static function getFirstNonTransparentPixel( bmd:BitmapData ) :P oint{
  2.     var r1:Rectangle = bmd.getColorBoundsRect( 0xff000000, 0, false );
  3.     if ( r1.width> 0 ){
  4.         var temp:BitmapData = new BitmapData( r1.width, 1, true, 0 );
  5.         temp.copyPixels( bmd, r1, new Point());
  6.         var r2:Rectangle = temp.getColorBoundsRect( 0xff000000, 0, false );
  7.         return r1.topLeft.add( r2.topLeft );
  8.     }
  9.     return null;
  10. }

Here (once again) be the results:

Test with image w:100,h:100
testFirstBitmapLooping() first:(x=30, y=30)
testFirstBitmapLooping() test took : 3 milliseconds
testFirstBitmapFloodFill() first:(x=30, y=30)
testFirstBitmapFloodFill() test took : 4 milliseconds
testFirstBitmapHitTest() first:(x=30, y=30)
testFirstBitmapHitTest() test took : 1 milliseconds
testFirstBitmapMario() first:(x=30, y=30)
testFirstBitmapMario() test took : 1 milliseconds

Test with image w:500,h:500
testFirstBitmapLooping() first:(x=150, y=150)
testFirstBitmapLooping() test took : 30 milliseconds
testFirstBitmapFloodFill() first:(x=150, y=150)
testFirstBitmapFloodFill() test took : 17 milliseconds
testFirstBitmapHitTest() first:(x=150, y=150)
testFirstBitmapHitTest() test took : 1 milliseconds
testFirstBitmapMario() first:(x=150, y=150)
testFirstBitmapMario() test took : 3 milliseconds

Test with image w:1000,h:1000
testFirstBitmapLooping() first:(x=300, y=300)
testFirstBitmapLooping() test took : 62 milliseconds
testFirstBitmapFloodFill() first:(x=300, y=300)
testFirstBitmapFloodFill() test took : 41 milliseconds
testFirstBitmapHitTest() first:(x=300, y=300)
testFirstBitmapHitTest() test took : 7 milliseconds
testFirstBitmapMario() first:(x=300, y=300)
testFirstBitmapMario() test took : 10 milliseconds

Test with image w:2000,h:2000
testFirstBitmapLooping() first:(x=600, y=600)
testFirstBitmapLooping() test took : 306 milliseconds
testFirstBitmapFloodFill() first:(x=600, y=600)
testFirstBitmapFloodFill() test took : 96 milliseconds
testFirstBitmapHitTest() first:(x=600, y=600)
testFirstBitmapHitTest() test took : 32 milliseconds
testFirstBitmapMario() first:(x=600, y=600)
testFirstBitmapMario() test took : 55 milliseconds

...so it turns out this wasn't an entirely futile exercise after all! I felt like a schmuck having misunderstood Mario, but I'm quite chuffed that the HitTest method is faster (even if by a practically irrelevant amount).

To be sure, I ran the two competitors 20 times on a bitmap of 2000 by 2000 and took the averages:

Mario Average: :36.7
HitTest Average: :21.2

In any case, I got to better terms with some of the BitmapData methods, which is something we all should aspire to anyway :) Thanks again Mario!

********** FINAL UPDATE (hopefully)************

Mario had a go at the HitTest version, and modified it slightly for even better performance:

Actionscript:
  1. public static function getFirstNonTransparentPixel( bmd:BitmapData ) :P oint{
  2.     var hit_rect:Rectangle=new Rectangle(0,0,bmd.width,1);
  3.     var p:Point = new Point();
  4.     for( hit_rect.y = 0; hit_rect.y <bmd.height; hit_rect.y++ ){
  5.         if( bmd.hitTest( p, 0x01, hit_rect) ){
  6.         var hit_bmd:BitmapData=new BitmapData( bmd.width, 1, true, 0 );
  7.         hit_bmd.copyPixels( bmd, hit_rect, p );
  8.         return hit_rect.topLeft.add( hit_bmd.getColorBoundsRect(0xFF000000, 0, false).topLeft );
  9.         }
  10.     }
  11.     return null;
  12. }

Mind you, I am testing this in debug player... I should use a release version with the release player, but for the sake of consistency with my previous posts I'll stick to my old "test suite".

Results:

HitTest Averages: :16
Mario Averages: :35.285714285714285
Hybrid Averages: :14.142857142857142

Now, finally, back to the code where I actually use this algo ;)

Get first non transparent pixel in a bitmap update

Tuesday, February 10th, 2009

On the way home I realized I had made a mistake in my previous blog post. In my hitTest() version, for some reason I had used a Rectangle which was the full size of the target bitmap. Like the FloodFill version, this "hit test" rectangle only needs to be one pixel in height. This means a lot less checking.

Behold the updated version:

Actionscript:
  1. public static function getFirstNonTransparentPixelHitTest(bmd:flash.display.BitmapData,test_fill_color:uint=0xFFFFFF00) :P oint{
  2.     var hit_rect:Rectangle=new Rectangle(0,0,bmd.width,1);
  3.     for(var i:uint=0;i<bmd.height;i++){
  4.         if(bmd.hitTest(new Point(),0x01,hit_rect)){
  5.             var hit_bmd:BitmapData=new BitmapData(bmd.width,1,true,0);
  6.             var m:Matrix=new Matrix(1,0,0,1,0,-i);
  7.             hit_bmd.draw(bmd,m);
  8.             hit_bmd.floodFill(0,0,test_fill_color);//use yellow, assume that the image tested is monochrome... (black and transparent)
  9.             var bounds:Rectangle=hit_bmd.getColorBoundsRect(0xFFFFFFFF,test_fill_color);
  10.             return new Point(bounds.width,i);
  11.         }
  12.         hit_rect.y++;
  13.     }
  14.     return null;
  15. }

Now behold the new results:

Test with image w:100,h:100
testFirstBitmapHitTest() first:(x=30, y=30)
testFirstBitmapHitTest() test took : 2 milliseconds
testFirstBitmapLooping() first:(x=30, y=30)
testFirstBitmapLooping() test took : 1 milliseconds
testFirstBitmapFloodFill() first:(x=30, y=30)
testFirstBitmapFloodFill() test took : 3 milliseconds

Test with image w:500,h:500
testFirstBitmapHitTest() first:(x=150, y=150)
testFirstBitmapHitTest() test took : 8 milliseconds
testFirstBitmapLooping() first:(x=150, y=150)
testFirstBitmapLooping() test took : 32 milliseconds
testFirstBitmapFloodFill() first:(x=150, y=150)
testFirstBitmapFloodFill() test took : 20 milliseconds

Test with image w:1000,h:1000
testFirstBitmapHitTest() first:(x=300, y=300)
testFirstBitmapHitTest() test took : 13 milliseconds
testFirstBitmapLooping() first:(x=300, y=300)
testFirstBitmapLooping() test took : 117 milliseconds
testFirstBitmapFloodFill() first:(x=300, y=300)
testFirstBitmapFloodFill() test took : 42 milliseconds

Test with image w:2000,h:2000
testFirstBitmapHitTest() first:(x=600, y=600)
testFirstBitmapHitTest() test took : 23 milliseconds
testFirstBitmapLooping() first:(x=600, y=600)
testFirstBitmapLooping() test took : 282 milliseconds
testFirstBitmapFloodFill() first:(x=600, y=600)
testFirstBitmapFloodFill() test took : 102 milliseconds

So the hitTest approach wins after all :)

***EDIT :

AND CLICK HERE TO READ UPDATE!

***