Flex/Flash/Actionscript

Using hitTestPoint or hitTest on Transparent PNG images

On a previous unrelated blog post, someone posted a comment that had to do with using the hitTestPoint() method with transparent images. I was about to respond by saying I don’t have any experience playing with hitTestPoint, I’m not sure why that question was posted to my unrelated post, and I don’t have an answer. While I was formulating my response in my head, I read up a bit on hitTestPoint (in the DisplayObject class) and hitTest (in BitmapData). And before I got done figuring out how to phrase “I don’t know, go ask someone else,” I thought that maybe I actually did know and figured it would be interesting to investigate a solution.

So here’s my take on DisplayObject.hitTestPoint() and BitmapData.hitTest(). At the end of the post is an example that does some hit testing on two images, one is saved as a transparent PNG and one as a SWF.

hitTestPoint


DisplayObject.hitTestPoint(x:Number, y:Number, shapeFlag:Boolean = false)

This method returns true if the point is over a transparent area of a bitmap image. Using the shapeFlag boolean makes it so the method returns false over an area of a vector image that has no shape. This is not the same thing as a transparent pixel of a bitmap image. It might be a little confusing in the documentation, but I think it makes sense (maybe it needs a sentence or two of clarification). Basically in a vector image you have no data in transparent areas. In a bitmap image there is a pixel there, and that pixel has an alpha value of 0. But that’s actually different than not having anything taking up that x,y coordinate. So the term “shapeFlag” does in fact only refer to shapes in vector images. Thank god they didn’t call it “transparentFlag” or something like that since that would make it even more confusing. It’s not about transparent pixels versus opaque pixels. It’s about “is there something there or not,” and a transparent pixel is still a pixel.

hitTest


BitmapData.hitTest(firstPoint:Point, firstAlphaThreshold:uint, 
         secondObject:Object, secondBitmapDataPoint:Point = null, 
         secondAlphaThreshold:uint = 1):Boolean

This function is confusing as hell, and I strongly suggest Adobe write some further documentation with a few examples on how to use this. This is a powerful function because you can test for a hit between a BitmapData object and multiple other types of objects (a Point, Rectangle, or another BitmapData). But it’s hard to figure out how to use it. Like, wtf does the first parameter mean (firstPoint)? I think it’s only used if you’re testing between one BitmapData and a second BitmapData. I was interested in testing a BitmapData against a point, so I just passed a point of 0,0 and it seemed to work right.

But anyway, griping about documentation aside, these two methods are different. If you’re interested in testing for a hit on a bitmap image and you don’t want to count the transparent areas, then you must use BitmapData.hitTest. You cannot use DisplayObject.hitTestPoint because that will return true for transparent pixels.

So I’ve made a simple utility class that solves this problem. I called it HitTester and what it does is it first checks for a hit using DisplayObject.hitTestPoint. If that returns true it means we’re at least within the bounding box of the image. Then it checks using BitmapData.hitTest. To do this it creates a temporary BitmapData object for the Image and runs the hitTest method.

To use the class you do something like this:

HitTester.realHitTest(image1, new Point(event.stageX, event.stageY));

The example below shows using the DisplayObject.hitTestPoint versus using my HitTester utility class. Move your mouse over the images and you can see the hit test results. Both images are basically the same, but I saved the left one as a transparent PNG and the right one is a SWF. You’ll see that the normal hitTest Point method works to accurately test for opaque areas of the SWF, but not for the PNG. The HitTest class basically just makes the hit testing of the PNG work like it does for the SWF.

View the source.

This movie requires Flash Player 9.

Standard

40 thoughts on “Using hitTestPoint or hitTest on Transparent PNG images

  1. Ray Greenwell says:

    Doug,

    Cute. Unfortunately, drawing a SWF into an offscreen bitmap so that I can examine the pixels is not going to cut it.
    Hell, if I could control all my bitmaps, the solution to this would be really easy. I’ve written my own class, called LessRetardedBitmap, that fixes the basic problem:


    import flash.display.Bitmap;
    import flash.geom.Point;

    public class LessRetardedBitmap extends Bitmap
    {
    public function LessRetardedBitmap (fullTard :Bitmap)
    {
    super(fullTard.bitmapData, fullTard.pixelSnapping, fullTard.smoothing);
    }

    override public function hitTestPoint (
    tx :Number, ty :Number, shapeFlag :Boolean = false) :Boolean
    {
    if (shapeFlag && bitmapData != null && parent != null) {
    var p :Point = globalToLocal(new Point(tx, ty));
    return bitmapData.hitTest(new Point(0, 0), 0xFF, p);

    } else {
    return super.hitTestPoint(tx, ty, shapeFlag);
    }
    }
    }

    The problem is that the native hit-testing done in the flash player for mouse events still will not use this. If I install a mouse listener on a sprite (which lets say contains child objects) then that sprite will get a mouseOver event. Even if I then do an extra test and figure out that the mouse pointer is over a transparent pixel, there’s no way to re-insert the mouse event, having it skip the sprite, and find out where the mouseOver should *really* land on some sprite behind it.

    If I’m in control of everything, I could handle this. I’d just install a mouse listener on some top-level container and then do my own custom hit testing on all the children. However, for the project I’m working on, we could be loading SWFs from another server, containing bitmaps that are entirely out of our control. It’s really disconcerting when transparent areas of these swfs count as hits under the mouse pointer. It’s possible that your HitTester would work for this (although, I think the security domain issues would prevent me from painting the SWF into the bitmapdata) but I fear that it will be too slow in any case.

    I guess it’s just a problem with how the flash player counts things as transparent, and I’ll just have to suck it up.

  2. Aha, so your problem actually isn’t with determining a hit on an object. You’ve got Sprite A in front of Sprite B and you want to have the mouse events fire for Sprite B if the mouse moves over a transparent part of Sprite A. That’s going to be tough.

    Apart from reading in your bitmaps and re-drawing them as vector images pixel-by-pixel… and I assume this simply isn’t an option based on performance. Although if you have a limited number of sprites you could theoretically do this during app initialization. Read in the bitmaps, re-draw as vector on a pixel-by-pixel basis, then enable cacheAsBitmap (so the performance after the redrawing is complete would be the same).

    But yeah, the real answer may very well be that you’re SOL 🙂

  3. Ray Greenwell says:

    Yeah, that’s the real answer, because I am not in control of the DisplayObjects being shown.

    I did just notice a bug in your HitTester, though. The realHitTest() function takes a DisplayObject parameter and then checks to see if it’s a BitmapData. Well, BitmapData is not a subclass of DisplayObject, what you mean to check for is Bitmap (with a non-null bitmapdata, etc.).

  4. You’re right about that mistake with trying to check if a DisplayObject is a BitmapData. I noticed that sometime after I published it, it should definitely be changed as you suggest.

    I was thinking about a solution to your situation, but it would probably be far too intensive for your purposes. But the basic idea would go like this:

    1. Load a DisplayObject.
    2. Create a BitmapData object from that DisplayObject
    3. Somehow create a vector representation of the bitmap as a Sprite, this would only have to be a vector shape that contains the non-transparent pixels, regardless of color. So you would need some sort of basic edge detection algorithm to make the vector shape. This would be the magic part that I don’t know how to do yet. You could replace all non-transparent pixels with black, then it might be easier to convert that to a vector shape. I don’t know.
    4. Create a Sprite object, use beginBitmapFill to paint the BitmapData.
    5. Set the hitArea of that Sprite to the vector object you created.

    So then you would have a sprite that looked the same as your original DisplayObject, but it would use the hitArea that wouldn’t include the transparent pixels. So the main problems are 1) generating the vector object from the DisplayObject and 2) performance might take too much a hit.

  5. Ray Greenwell says:

    Doug, the only problem would be if the DisplayObject is a MovieClip, you’d probably want to update the hitArea sprite every frame, and at that point you’re worse off than the original hack, which at least only updated the offscreen bitmap when you wanted to hit test…

    My problem is that for the app I’m writing, some of the DisplayObjects on the screen could be SWFs uploaded by users. The problem arises when these SWFs (which are in different security domains) use a Bitmap internally. There is nothing I can do to even know that there is a bitmap in there, my hit testing cannot enter the sprite. I’m screwed: any transparent image areas in that sprite will count as a hit.

    I think I’m just screwed.

  6. Luke McLean says:

    Hi Doug,

    I’m now trying to create desktop like icons and labels that behave to drag and drop events like the desktop icons on http://www.desktoptwo.com. Transparent icons and labels however I’m so new to this stuff that I find myself a bit lost. Could you please give me some pointers on how you would accomplish this.. and let me know your hourly rate!

  7. Samy says:

    Hi,

    I would know if this code could be transcripted into Flash 8 ? I tried to do it, but there where several errors and incompatibilities. (if it’s possible, is someone able to help me?)

    Thx.

    Samy

  8. Erik Rydeman says:

    Wohoo, this solved all my problems. Well done!

    You’ve saved me the trouble of not realizing hitTest could be done for transparent PNG-images. 😉

  9. Samy says:

    Hello!

    At the beginning I waited a response, today I thought why not now? And it was here! THX a lot Eric, the class of Grant Skinner is perfect to do what I want!

    The new possibilities of Flex 3 are interesting…hoping Flex Builder 3 will be released soon, playing with the SDK is not for me…maybe in a couple of years! 😉

    Samy

  10. madcalf says:

    Thanks for writing the HitTester and posting it! It’s a real lifesaver! I was really tearing my hair out there when i found out there was nothing built-in to flash to handle hitTesting on transparent bitmaps…
    thanks again.
    d

  11. Vinoth says:

    How a HitTest can be done for UIcomponent? Any other method equivalent to hitTest?

    Thanks,
    Vinoth

  12. Jacob Wright says:

    I know this comment is a little after the fact, but in reply to Ray’s original problem you could do your own mouse events. You could write your own MouseEventCatcher class that listens to mouse events on the stage with the capture flag set to true. Then if the mouse event shouldn’t be firing on an object because it’s transparent or whatever other requirements you might have, you can find the object it should be on using hit tests up through the display list and dispatch a new mouse event from the new target. I got it almost working except for ROLL_OVER and ROLL_OUT events. I can’t test where the mouse was, just where it currently is.

  13. jackflit says:

    you can draw only one pixel to improve you performance like this

    var bmapData:BitmapData = new BitmapData(1, 1, true, 0x00000000);
    var m:Matrix = new Matrix();
    var localPt:Point = object.globalToLocal(point);
    m.tx = -localPt.x;
    m.ty = -localPt.y;
    bmapData.draw(object, m);

    var returnVal:Boolean = bmapData.hitTest(new Point(0,0), 128, new Point(0, 0));

    bmapData.dispose();

  14. Martin says:

    Hello,

    what if instead of the mouseEvent there is another Image object!?

    how would I have to make it?

    Thanks!

  15. Martin says:

    I cannot find examples where real hitTest pixel collision is detected.

    Seems I have to use this method but when it comes to detect collision between two objects moving doesn’t seem to work.

    Can anyone apart from the owner of this blog to tell me what can I do?

    Thanks!

  16. @Martin, yeah, the blog is active in the sense that I actively write posts and monitor what people say. I don’t necessarily have the time to answer all the questions people ask (especially if they’re not directly related to what I posted about). So I don’t have an answer for you, this entry was about detecting a mouse rollover or click event, that’s what the term “hit test” in this context means. If you’re trying to get effective hit testing using two transparent png images as they collide with each other, I don’t have an easy answer for you. You can try posting your question to the flexcoders mailing list here: http://tech.groups.yahoo.com/group/flexcoders/

  17. Martin says:

    Hi Doug,

    actually both are related as you can see. Instead of a mouse pointer there is another Image in my case. So I don’t know why you said what you said.

    Thanks for referring to that site

  18. marta says:

    Here is another performance fix 🙂

    private var hitTestBitmapBuffer:Array = new Array();

    public function realHitTest(object:DisplayObject, point:Point):Boolean
    {
    /* If we’re already dealing with a BitmapData object then we just use the hitTest
    * method of that BitmapData.
    */
    if(object is BitmapData) {
    return (object as BitmapData).hitTest(new Point(0,0), 0, object.globalToLocal(point));
    }
    else {

    /* First we chack if the hitTestPoint method returns false. If it does, that
    * means that we definitely do not have a hit, so we return false. But if this
    * returns true, we still don’t know 100% that we have a hit because it might
    * be a transparent part of the image.
    */
    if(!object.hitTestPoint(point.x, point.y, true)) {
    return false;
    }
    else {
    /* So now we make a new BitmapData object and draw the pixels of our object
    * in there. Then we use the hitTest method of that BitmapData object to
    * really find out of we have a hit or not.
    */

    var bmapData:BitmapData;
    bmapData = this.hitTestBitmapBuffer[object];
    if(bmapData == null)
    {
    bmapData = new BitmapData(object.width, object.height, true, 0x00000000);
    bmapData.draw(object, new Matrix());
    }

    var returnVal:Boolean = bmapData.hitTest(new Point(0,0), 0, object.globalToLocal(point));

    //bmapData.dispose();
    this.hitTestBitmapBuffer[object] = bmapData;

    return returnVal;
    }
    }
    }

  19. Pingback: Durch transparenten MovieClip klicken [Flash 9 (CS3)] @ Flashhilfe.de - Flash Forum

  20. oh hey Doug, I never saw this post.

    I think we chatted about my utility IneractivePNG, which fixes overlapping PNGS so they simply act like there’s no clear areas.

    Old news really, but I just had someone point to both of our posts so maybe it’s still relevant. If anyone is interested, click my name on this post to check it out!

    🙂
    Moses

  21. LarryH says:

    This all seems well and good, but if you want to have some fun, try figuring out how to do this during a drag and drop operation…..what a pain in the Arse that was.

    Adobe was kind enough to not allow access to the dragProxy or even the dragged item without significant pain in the, well you know.

    Adobe, come on!

  22. Roman says:

    Hi,

    my png is located at its relative position (0, -150). I need exact this position for rotation.
    Your class only works when the position of the png is (0,0) in its movieclip. How can I solve this.

    Roman

  23. I’ve done something similar (on BitmapData) but not with hitTest() rather, getPixel32().
    I have several layers of Image objects so I run the following in a loop through children (from the top down) until I hit a non transparent pixel:
    private function amOver(target:Image, overX:int, overY:int):Boolean
    {
    var bmp:Bitmap = target.content as Bitmap;
    var returnVal:Boolean = false;
    var color:uint;
    var alphaThreshold:uint = 0xFF000000;
    if (!bmp)
    return false;
    else {
    color = bmp.bitmapData.getPixel32(overX,overY);
    returnVal = (color < alphaThreshold) ? false : true;
    return returnVal;
    }
    }
    If you’re over a non transparent pixel in the image, returns true, otherwise returns false. In my looping, if it returns false, I go down to the next sibling. If I make it through all siblings, I can go to the parent and get my parents siblings. Really, you can have as many Images as you want in your app, and technically (if you set your “looping” up right), you can find the owner (Image) of the pixel with a color < 0xFF000000 (any pixel with alpha higher than 0)
    You can adjust the alphaThreshold variable to use a value of something with an alpha of less than 1 (so you can still mouse over shade if you want).
    Hope this helps someone!

  24. qzex says:

    Thanks so much. I was banging my head on my desk for a few hours wondering why the hell hitTestPoint wasn’t working after creating an excruciating workaround for the lack of the shapeFlag argument in hitTestObject.

  25. @roman, this fixed the offset issue for me. Not sure if it’s perfect, but it works.

    var bounds:Rectangle = object.getBounds(object.parent);
    bounds.x = object.x-bounds.x;
    bounds.y = object.y-bounds.y;
    var gpoint:Point = object.globalToLocal(point);
    gpoint.x += bounds.x;
    gpoint.y += bounds.y;

    var bmapData:BitmapData = new BitmapData(bounds.width, bounds.height, true, 0x00000000);
    bmapData.draw(object, new Matrix(1,0,0,1,bounds.x, bounds.y));
    var returnVal:Boolean = bmapData.hitTest(new Point(0,0), 0, gpoint);

    bmapData.dispose();

  26. Dragos Raducanu says:

    Hi,
    I don’t know if you still keep track of this post, but I would really need your help.
    As I know so far flash.display.DisplayObject is not available in ActionScript 2.0. Unfortunately for me, my project is done in AS2.0. Any way I could use your class in AS2.0?

    Thank you for your time.

  27. aaa says:

    i dont’t read this shit. too many fnords.

    this worked for me tranparent png many layers hit test shit
    i stole this somewhere else

    this.addEventListener(MouseEvent.MOUSE_MOVE,onMouseMove);

    function onMouseMove(e:MouseEvent):void
    {
    var pt:Point = myBitmap.globalToLocal( new Point(e.stageX, e.stageY) );
    if(myBitmap.bitmapData.hitTest( new Point(), 1(=100% opacity), pt))
    {
    trace(“HIT!”);
    //do your stuff here
    }
    }

    this for many layers

    for(var i:int; i<this.numChildren; i++)

  28. All I can say is: I love you Doug !

    After browsing the net for hours about this topic, this class is a dream come true !

    Thanks !

    DM

    @ Dragos: hitTest is a native function in AS2 is it not?

  29. Hi. This is a great article and exactly what I was looking for.

    I am having trouble understanding the source code however.

    Are you using a created BitMapDataObject, checking it, then creating a BitMapObject of the already created bitmapobject and measuring again? Sorry, Ive only been using AS3 for 4 months, but loving it.

    Im working on a project at the moment, and my understanding of this is critical.

  30. James says:

    this technique can use in a game?., for example a maze game., i’ll make a map in one .png file, then the other object can pass on the non pixelated parts of the map., do i make sense?

  31. Pingback: HitTest Object bei teil transparenten pngs? - Flashforum

  32. Pingback: SimpleButton mit transparent.png als hitTestState - Flashforum

  33. seife says:

    Hi,
    what needs to be changed in the script if bitmap isn’t placed in point 0,0 but in the centre of the movie clip?

Comments are closed.