Erik’s Brain

The reward of a thing well done...

Hit Area of External Assets With Alpha Channels (PNGs and SWFs)

Let’s say you are loading in an external asset that has an alpha channel, such as a PNG or a SWF*. Now let’s say that you want that asset to act like a button, with rollover and click actions. No problem. No reason you can’t do that.

But, actually, there is a problem. The hit area for that external asset will be its entire bounding box.

Take this octy image for example:

Octy

Even if you rolled over the top left of the image where it’s totally transparent, it will still trigger rollover actions, which is certainly not ideal and could potentially be very confusing for a user. This isn’t what happens if you have that image sitting in the library and you then add it to the stage at run time. But c'mon, we need it to work right with externally loaded assets! Of course I wouldn’t be writing this if I was just bitching and moaning. I’ve got a solution!

First, here’s the code in its entirety:

package
{   
    import flash.display.*;
    import flash.events.*;
    import flash.net.*;
    import flash.filters.*;

    public class Main extends Sprite
    {
        // this will hold the external asset and will act like a button
        public var s:Sprite

        public function Main()
        {
            init();
        }

        private function init():void
        {
            var l:Loader = new Loader();
            l.contentLoaderInfo.addEventListener(Event.INIT, assetLoaded);
            l.load(new URLRequest("octy.png"))
        }

        private function assetLoaded(e:Event):void
        {
            // copy the content of the loaded asset into a new BitmapData object
            var bmpData:BitmapData = new BitmapData(e.target.content.width, e.target.content.height, true, 0);
            bmpData.draw(e.target.content, null, null, null, null, true);
            var newBmp:Bitmap = new Bitmap(bmpData, "auto", true);

            s = new Sprite();
            s.x = s.y = 50;
            s.addChild(newBmp);
            addChild(s);

            // make our Sprite act like a button
            s.addEventListener(MouseEvent.CLICK, clickHandler)
            s.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler)
            s.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler)
            s.buttonMode = true;
        }

        // when you roll over the Sprite, create a MOUSE_MOVE listener
        private function rollOverHandler(e:MouseEvent):void
        {
            s.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler)
        }

        // when you roll off of the Sprite, remove the MOUSE_MOUSE listener
        private function rollOutHandler(e:MouseEvent):void
        {
            s.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler)
            hideGlow();
        }

        // constantly check the alpha value of the pixel under the mouse pointer
        private function mouseMoveHandler(e:MouseEvent):void
        {
            var av:int = getAlpha(s)
            // if the pixel under the mouse isn't transparent,
            // show the hand cursor and turn on the image's glow 
            if (av > 0)
            {
                s.useHandCursor = true;
                showGlow()
            }
            // otherwise hide the hand cursor and turn off the glow
            else
            {
                s.useHandCursor = false;
                hideGlow()
            }
        }

        private function clickHandler(e:MouseEvent):void
        {
            var av:int = getAlpha(s)
            if (av > 0)
            {
                //do whatever it should do on click!
            }
        }

        private function showGlow():void
        {
            s.filters = [new GlowFilter()]
        }

        private function hideGlow():void
        {
            s.filters = []
        }

        // a little helper function that returns the alpha value of
        // the pixel under the mouse pointer
        private function getAlpha($t:Sprite):int
        {
            var bmd:BitmapData = Bitmap($t.getChildAt(0)).bitmapData;
            var av:int = (bmd.getPixel32($t.mouseX, $t.mouseY)) >> 24 & 0xFF
            return av;
        }
    }
}

The idea is pretty simple. Since Flash isn’t smart enough to handle things the way we’d like, we’ll just have to do it ourselves. To do that we’ll use BitmapData.getPixel32() to find the alpha value of the pixel underneath the mouse whenever the mouse moves. If that pixel’s alpha value is 0, then we’ll act like we aren’t over the button. But if the alpha value is greater than 0, then we will act accordingly by showing the hand cursor and triggering any cool rollover effects (in this case just a simple Glow Filter).

private function mouseMoveHandler(e:MouseEvent):void
{
    var av:int = getAlpha(s)
    // if the pixel under the mouse isn't transparent,
    // show the hand cursor and turn on the image's glow 
    if (av > 0)
    {
        s.useHandCursor = true;
        showGlow()
    }
    // otherwise hide the hand cursor and turn off the glow
    else
    {
        s.useHandCursor = false;
        hideGlow()
    }
}

But since you don’t want to waste CPU cycles by constantly checking the mouse position when you don’t need to, we’ll only enable that MOUSE_MOVE event listener when the mouse is over the bounding box of the externally loaded asset.

private function rollOverHandler(e:MouseEvent):void
{
    s.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler)
}

When you roll off of the bounding box, remove the listener.

private function rollOutHandler(e:MouseEvent):void
{
    s.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler)
    hideGlow();
}

The same kind of logic is used for the CLICK event handler. You only actually do anything if the mouse isn’t over the transparent part of the image.

private function clickHandler(e:MouseEvent):void
{
    var av:int = getAlpha(s)
    if (av > 0)
    {
        //do whatever it should do on click!
    }
}

So that should give you a pretty good idea of how to do this in your own project where needed. Hopefully someone in need finds this post and saves themselves a lot of effort!

*A word on SWFs as images

SWFs are a great alternative to PNGs if you need external images to have nice alpha channels. By using a tool like Fireworks you can open a PNG with a nice alpha channel and then save it as a SWF. This is great because by saving the PNG as a SWF, you keep the alpha channel AND get JPEG compression which will give you MUCH smaller file sizes. The PNG to SWF conversion is an extra step (why why why can’t you just save to SWF direct from Photoshop?) but it might be worth it depending on your situation.

Comments