Rendering an image outline stroke

While Flash 8 image filters added some excellent (and easy to use) graphics capabilities to the Flash display arsenal, I feel like there was a blatant omission from the filter set… what about a simple outline filter that will render a border around an image? Of course, you can fake the effect with a glow filter set to 1000% strength, but that still has a soft edge and doesn’t accommodate aliased graphics. So, I’ve been working on a script to render an outlined image based on the source image’s alpha channel. Here’s the code thus far:

Save as: com/gmacwilliam/graphics/ImageOutline.as

package com.gmacwilliam.graphics
{
    import flash.display.IBitmapDrawable;
    import flash.display.BitmapData;
    import flash.display.Bitmap;
    import flash.display.DisplayObject;
    import flash.geom.Rectangle;
    import flash.geom.Point;

    public class ImageOutline
    {
        private static var _color:uint;
        private static var _hex:String = "";
        private static var _alpha:Number = 1;
        private static var _weight:Number = 2;
        private static var _brush:Number = 4;

        /**
        * Renders a Bitmap display of any DisplayObject with an outline drawn around it.
        * @note: see param descriptions on "outline" method below.
        */
        public static function renderImage(src:IBitmapDrawable, weight:int, color:uint, alpha:Number=1, antialias:Boolean=false, threshold:int=150):Bitmap
        {
            var w:int = 0;
            var h:int = 0;

            // extract dimensions from actual object type.
            // (unfortunately, IBitmapDrawable does not include width and height getters.)
            if (src is DisplayObject)
            {
                var dsp:DisplayObject = src as DisplayObject;
                w = dsp.width;
                h = dsp.height;
            }
            else if (src is BitmapData)
            {
                var bmp:BitmapData = src as BitmapData;
                w = bmp.width;
                h = bmp.height;
            }

            var render:BitmapData = new BitmapData(w, h, true, 0x000000);
            render.draw(src);

            return new Bitmap(ImageOutline.outline(render, weight, color, alpha, antialias, threshold));
        }

        /**
        * Renders an outline around a BitmapData image.
        * Outline is rendered based on image's alpha channel.
        * @param: src = source BitmapData image to outline.
        * @param: weight = stroke thickness (in pixels) of outline.
        * @param: color = color of outline.
        * @param: alpha = opacity of outline (range of 0 to 1).
        * @param: antialias = smooth edge (true), or jagged edge (false).
        * @param: threshold = Alpha sensativity to source image (0 - 255). Used when drawing a jagged edge based on an antialiased source image.
        * @return: BitmapData of rendered outline image.
        */
        public static function outline(src:BitmapData, weight:int, color:uint, alpha:Number=1, antialias:Boolean=false, threshold:int=150):BitmapData
        {
            _color = color;
            _hex = _toHexString(color);
            _alpha = alpha;
            _weight = weight;
            _brush = (weight * 2) + 1;

            var copy:BitmapData = new BitmapData(src.width+_brush, src.height+_brush, true, 0x00000000);

            for (var iy:int=0; iy <= src.height; iy++)
            {
                for (var ix:int=0; ix <= src.width; ix++)
                {
                    // get current pixel's alpha component.
                    var a:Number = (src.getPixel32(ix, iy) >> 24 & 0xFF);

                    if (antialias)
                    {
                        // if antialiasing,
                        // draw anti-aliased edge.
                        _antialias(copy, ix, iy, a);
                    }
                    else if (a > threshold)
                    {
                        // if aliasing and pixel alpha is above draw threshold,
                        // draw aliased edge.
                        _alias(copy, ix, iy);
                    }
                }
            }

            // merge source image display into the outline shape's canvas.
            copy.copyPixels(src, new Rectangle(0, 0, copy.width, copy.height), new Point(_weight, _weight), null, null, true);
            return copy;
        }

        /**
        * Renders an antialiased pixel block.
        */
        private static function _antialias(copy:BitmapData, x:int, y:int, a:int):BitmapData
        {
            if (a > 0)
            {
                for (var iy:int = y; iy < y+_brush; iy++)
                {
                    for (var ix:int = x; ix < x+_brush; ix++)
                    {
                        // get current pixel's alpha component.
                        var px:Number = (copy.getPixel32(ix, iy) >> 24 & 0xFF);

                        // set pixel if it's target adjusted alpha is greater than the current value.
                        if (px < (a * _alpha)) copy.setPixel32(ix, iy, _parseARGB(a * _alpha));
                    }
                }
            }
            return copy;
        }

        /**
        * Renders an aliased pixel block.
        */
        private static function _alias(copy:BitmapData, x:int, y:int):BitmapData
        {
            copy.fillRect(new Rectangle(x, y, _brush, _brush), _parseARGB(_alpha * 255));
            return copy;
        }

        /**
        * Utility to parse an ARGB value from the current hex value
        * Hex string is cached on the class so that it does not need to be recalculated for every pixel.
        */
        private static function _parseARGB(a:int):uint
        {
            return parseInt("0x"+ a.toString(16) + _hex);
        }

        /**
        * Utility to parse a hex string from a hex number.
        */
        private static function _toHexString(hex:uint):String
        {
            var r:int = (hex >> 16);
            var g:int = (hex >> 8 ^ r << 8);
            var b:int = (hex ^ (r << 16 | g << 8));

            var red:String = r.toString(16);
            var green:String = g.toString(16);
            var blue:String = b.toString(16);

            red = (red.length < 2) ? "0" + red : red;
            green = (green.length < 2) ? "0" + green : green;
            blue = (blue.length < 2) ? "0" + blue : blue;
            return (red + green + blue).toUpperCase();
        }
    }
}

The class has two public render methods: “outline()” receives and returns raw BitmapData data, so requires some pre and post methodology to be used in a display pattern. However, the “renderImage()” method is written to be display-ready. Feed it a reference to any DisplayObject and some render parameters, and it will return a Bitmap object that can be added directly to the stage. All render parameters are documented within the class comments, but for quick reference, the basic params include: [thickness, color, alpha, antialias]. A simple implementation would look like this:

import com.gmacwilliam.graphics.ImageOutline;
var outline:Bitmap = ImageOutline.renderImage(myMovieClipRef, 2, 0xFF0000, 1, false);
addChild(outline);

It’s a work in progress. It’ll render a pixel-perfect outline on aliased graphics, which is a huge plus if you need to work with jagged edges. The anti-aliasing methodology has been quite a challenge and still plateaus along some curves, in which case the 1000% glow filter may still look better. If anyone has experience in rendering anti-aliased pixels, please feel free to comment with tech notes!

Advertisements

2 comments so far

  1. makc on

    you could try to utilize threshold to make b/w image from alpha, glow it, and then use blending modes to merge back into original image.

  2. top mistakes on

    Your blog is really cool to me and your topics are very relevant. I was browsing around and came across something you might find interesting. I was guilty of 3 of them with my sites. “99% of blog owners are doing these five errors”. http://tinyurl.com/7dtdnz9 You will be suprised how easy they are to fix.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: