Sure thing! I dunno how efficient it is, but I don’t mind sharing.
I do think my “distance” values differ slightly than how Flash/OpenFL implemented them, so you might have to play around with the implementation or change my code. Currently I only have basic strokes, glows, and drop shadows implemented.
Here’s a sample file to test the work around:
package;
import haxe.Timer;
import openfl.display.Sprite;
import openfl.text.TextField;
import openfl.text.TextFieldAutoSize;
import openfl.text.TextFormat;
/**
* ...
* @author Tamar Curry
*/
class Main extends Sprite
{
private var tf:TextField;
private var filterSprite:FilterSprite;
public function new()
{
super();
tf = new TextField();
tf.width = 1;
tf.height = 1;
tf.autoSize = TextFieldAutoSize.LEFT;
var format:TextFormat = tf.defaultTextFormat;
format.size = 40;
tf.defaultTextFormat = format;
tf.x = 20;
tf.y = 20;
filterSprite = new FilterSprite();
filterSprite.source = tf;
filterSprite.x = tf.x;
filterSprite.y = tf.y;
filterSprite.addStroke( 2, 0xffffff );
filterSprite.addGlow( 4, 0x990000, 1, 8, 8 );
filterSprite.addDropShadow();
addChild( filterSprite );
addChild( tf );
newText();
var timer:Timer = new Timer(1000);
timer.run = newText;
}
private function newText():Void
{
tf.text = "Hello, World " + Math.round( Math.random() * 999999 );
filterSprite.render();
}
}
And then here’s the source for the filter workarounds. The regular Blur filter still works in HTML5 builds, so I use it as an optional softening effect if desired:
package ;
import openfl.display.Bitmap;
import openfl.display.BitmapData;
import openfl.display.DisplayObject;
import openfl.display.PixelSnapping;
import openfl.display.Sprite;
import openfl.filters.BitmapFilter;
import openfl.filters.BlurFilter;
import openfl.geom.ColorTransform;
import openfl.geom.Matrix;
import openfl.geom.Point;
import openfl.geom.Rectangle;
/**
* ...
* @author Tamar Curry
*/
class FilterSprite extends Sprite
{
public var source:DisplayObject;
private var _holder:Sprite;
private var _matrix:Matrix;
private var _bitmaps:Array<Bitmap>;
private var _filterEffects:Array<IFilterEffect>;
public var _sourceRender:BitmapData;
public var hasRender(default, null):Bool;
// -----------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------
public function new()
{
super();
_filterEffects = [];
_bitmaps = [];
_matrix = new Matrix();
hasRender = false;
_holder = new Sprite();
addChild(_holder);
this.mouseEnabled = false;
this.mouseChildren = false;
}
// -----------------------------------------------------------------------------------------------
public function addStroke(distance:Int, rgbColor:Int, alpha:Float = 1):Void
{
_filterEffects.push( new GlowEffect(distance, rgbColor, alpha, 0, 0) );
addBitmap();
}
// -----------------------------------------------------------------------------------------------
public function addGlow(distance:Int, rgbColor:Int, alpha:Float = 1, blurX:Float = 0, blurY:Float = 0):Void
{
_filterEffects.push( new GlowEffect(distance, rgbColor, alpha, blurX, blurY) );
addBitmap();
}
// -----------------------------------------------------------------------------------------------
public function addDropShadow(distance:Float = 4, angle:Float = 45, rgbColor:Int = 0, alpha:Float = 1, blurX:Float = 4, blurY:Float = 4):Void
{
_filterEffects.push( new DropShadowEffect(distance, angle, rgbColor, alpha, blurX, blurY) );
addBitmap();
}
// -----------------------------------------------------------------------------------------------
private function addBitmap():Void
{
var bitmap:Bitmap = new Bitmap(null, PixelSnapping.AUTO, true);
_holder.addChildAt(bitmap, 0);
_bitmaps.push(bitmap);
}
// -----------------------------------------------------------------------------------------------
public function clear():Void
{
_holder.removeChildren();
hasRender = false;
for ( f in _filterEffects ) {
f.dispose();
}
_bitmaps.splice(0, _bitmaps.length);
_filterEffects.splice(0, _filterEffects.length);
}
// -----------------------------------------------------------------------------------------------
public function adjustPosition():Void
{
if ( source == null ) {
return;
}
var rect:Rectangle = source.getBounds(null);
_holder.x = rect.x;
_holder.y = rect.y;
}
// -----------------------------------------------------------------------------------------------
public function render():Void
{
_matrix.identity();
hasRender = true;
var w:Float = 2;
var h:Float = 2;
if ( source != null ) {
var rect:Rectangle = source.getBounds(null);
_matrix.scale( source.scaleX, source.scaleY );
_matrix.translate( -rect.x, -rect.y );
w = source.width;
h = source.height;
}
var sW:Int = Math.ceil(w);
var sH:Int = Math.ceil(h);
_sourceRender = FilterEffectFuncs.checkBitmapData( _sourceRender, sW, sH );
if ( source != null ) {
_sourceRender.draw( source, _matrix, null, null, null, true );
}
for ( bitmap in _bitmaps ) {
bitmap.x = 0;
bitmap.y = 0;
bitmap.filters = null;
bitmap.bitmapData = null;
}
var len:Int = _filterEffects.length;
var xOffset:Float = 0;
var yOffset:Float = 0;
var pixels:BitmapData = _sourceRender;
for ( i in 0...len ) {
if ( source == null ) {
_filterEffects[i].clear();
}
else {
_filterEffects[i].render(pixels, _bitmaps[i]);
w = _filterEffects[i].bitmapData.width;
h = _filterEffects[i].bitmapData.height;
_bitmaps[i].filters = _filterEffects[i].getFilters();
_matrix.identity();
_bitmaps[i].x += xOffset;
_bitmaps[i].y += yOffset;
xOffset = _bitmaps[i].x;
yOffset = _bitmaps[i].y;
pixels = _filterEffects[i].bitmapData;
}
}
adjustPosition();
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
class FilterEffectFuncs {
// -----------------------------------------------------------------------------------------------
public static function getColorTransform(RGBColor:Int):ColorTransform
{
var colorTransform:ColorTransform = new ColorTransform();
colorTransform.redOffset = ( RGBColor >> 16 );
colorTransform.greenOffset = ( (RGBColor >> 8) & 0xff);
colorTransform.blueOffset = (RGBColor & 0xff);
colorTransform.redMultiplier = 0;
colorTransform.greenMultiplier = 0;
colorTransform.blueMultiplier = 0;
return colorTransform;
}
// -----------------------------------------------------------------------------------------------
public static function checkBitmapData(bitmapData:BitmapData, newWidth:Int, newHeight:Int):BitmapData
{
if ( bitmapData == null ) {
bitmapData = new BitmapData(newWidth, newHeight, true, 0);
}
else if ( bitmapData.width < newWidth || bitmapData.height < newHeight ) {
bitmapData.dispose();
bitmapData = new BitmapData(newWidth, newHeight, true, 0);
}
else {
bitmapData.fillRect( bitmapData.rect, 0 );
}
return bitmapData;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
interface IFilterEffect {
var bitmapData(default, null):BitmapData;
function clear():Void;
function render(pixels:BitmapData, bitmap:Bitmap):Void;
function getFilters():Array<BitmapFilter>;
function dispose():Void;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
class DropShadowEffect implements IFilterEffect
{
private var _xDist:Float;
private var _yDist:Float;
private var _theta:Float;
private var _pt:Point;
private var _filter:Array<BitmapFilter>;
private var _colorTransform:ColorTransform;
public var rgbColor(default, null):Int;
public var blurX(default, null):Float;
public var blurY(default, null):Float;
public var alpha(default, null):Float;
public var angle(default, null):Float;
public var distance(default, null):Float;
public var bitmapData(default, null):BitmapData;
// -----------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------
public function new(distance:Float, angle:Float, rgbColor:Int, alpha:Float, blurX:Float, blurY:Float) {
this.distance = distance;
this.angle = angle;
this.rgbColor = rgbColor;
this.alpha = alpha;
this.blurX = blurX;
this.blurY = blurY;
_pt = new Point();
_theta = Math.PI * angle / 180;
_xDist = Math.cos(_theta) * distance;
_yDist = Math.sin(_theta) * distance;
_colorTransform = FilterEffectFuncs.getColorTransform(rgbColor);
_filter = null;
if ( blurX != 0 || blurY != 0 ) {
_filter = [ new BlurFilter(blurX, blurY) ];
}
}
// -----------------------------------------------------------------------------------------------
public function dispose():Void
{
if ( bitmapData != null ) {
bitmapData.dispose();
bitmapData = null;
}
}
// -----------------------------------------------------------------------------------------------
public function clear():Void
{
if ( bitmapData != null ) {
bitmapData.fillRect( bitmapData.rect, 0 );
}
}
// -----------------------------------------------------------------------------------------------
public function getFilters():Array<BitmapFilter>
{
return _filter;
}
// -----------------------------------------------------------------------------------------------
public function render(pixels:BitmapData, bitmap:Bitmap):Void
{
bitmapData = FilterEffectFuncs.checkBitmapData( bitmapData, pixels.width, pixels.height );
bitmapData.copyPixels( pixels, pixels.rect, _pt, null, null, true );
bitmapData.colorTransform( bitmapData.rect, _colorTransform );
bitmap.bitmapData = bitmapData;
bitmap.smoothing = true;
bitmap.x = _xDist;
bitmap.y = _yDist;
bitmap.alpha = alpha;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
class GlowEffect implements IFilterEffect
{
private var _pt:Point;
private var _filter:Array<BitmapFilter>;
private var _colorTransform:ColorTransform;
public var alpha(default, null):Float;
public var blurX(default, null):Float;
public var blurY(default, null):Float;
public var rgbColor(default, null):Int;
public var distance(default, null):Int;
public var bitmapData(default, null):BitmapData;
// -----------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------
public function new(distance:Int, rgbColor:Int, alpha:Float, blurX:Float, blurY:Float)
{
this.distance = distance;
this.rgbColor = rgbColor;
this.alpha = alpha;
this.blurX = blurX;
this.blurY = blurY;
_pt = new Point();
_filter = null;
_colorTransform = FilterEffectFuncs.getColorTransform(rgbColor);
if ( blurX != 0 || blurY != 0 ) {
_filter = [ new BlurFilter(blurX, blurY) ];
}
}
// -----------------------------------------------------------------------------------------------
public function dispose():Void
{
if ( bitmapData != null ) {
bitmapData.dispose();
bitmapData = null;
}
}
// -----------------------------------------------------------------------------------------------
public function clear():Void
{
if ( bitmapData != null ) {
bitmapData.fillRect( bitmapData.rect, 0 );
}
}
// -----------------------------------------------------------------------------------------------
public function getFilters():Array<BitmapFilter>
{
return _filter;
}
// -----------------------------------------------------------------------------------------------
public function render(pixels:BitmapData, bitmap:Bitmap):Void
{
if ( distance <= 0 ) {
return;
}
var bitmapW:Int = pixels.width + distance + distance;
var bitmapH:Int = pixels.height + distance + distance;
bitmapData = FilterEffectFuncs.checkBitmapData( bitmapData, bitmapW, bitmapH );
var skipDrawing:Bool = false;
var maxDist:Int = (distance * 2);
for ( xOffset in 0...(maxDist+1) ) {
for ( yOffset in 0...(maxDist+1) ) {
skipDrawing = false;
// don't draw the corners, because that makes it look rounder
skipDrawing = skipDrawing || (xOffset == 0 && yOffset == 0);
skipDrawing = skipDrawing || (xOffset == 0 && yOffset == maxDist);
skipDrawing = skipDrawing || (xOffset == maxDist && yOffset == 0);
skipDrawing = skipDrawing || (xOffset == maxDist && yOffset == maxDist);
if ( !skipDrawing ) {
_pt.x = xOffset;
_pt.y = yOffset;
bitmapData.copyPixels( pixels, pixels.rect, _pt, null, null, true );
}
}
}
bitmapData.colorTransform( bitmapData.rect, _colorTransform );
bitmap.bitmapData = bitmapData;
bitmap.smoothing = true;
bitmap.alpha = alpha;
bitmap.x = -distance;
bitmap.y = -distance;
}
}