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!