diff --git a/exporter/src/main/as/flump/SwfTexture.as b/exporter/src/main/as/flump/SwfTexture.as index ebc8b58..7c0993a 100644 --- a/exporter/src/main/as/flump/SwfTexture.as +++ b/exporter/src/main/as/flump/SwfTexture.as @@ -6,11 +6,15 @@ package flump { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.DisplayObject; +import flash.display.DisplayObjectContainer; import flash.display.MovieClip; import flash.display.Sprite; import flash.display.StageQuality; +import flash.filters.ColorMatrixFilter; +import flash.geom.Matrix; import flash.geom.Point; import flash.geom.Rectangle; +import flump.xfl.XflMovie; import flump.executor.load.LoadedSwf; import flump.mold.MovieMold; @@ -19,11 +23,11 @@ import flump.xfl.XflTexture; public class SwfTexture { - public var symbol :String; - public var origin :Point; - public var w :int, h :int, a :int; - public var scale :Number; - public var quality :String; + public function get symbol():String { return _symbol; } + public function get origin():Point { return new Point(_origin.x*_scale, _origin.y*_scale); } + public function get w():int { return Math.ceil(_w*_scale); } + public function get h():int { return Math.ceil(_h*_scale); } + public function get a():int { return this.w*this.h; } public static function fromFlipbook (lib :XflLibrary, movie :MovieMold, frame :int, quality :String = StageQuality.BEST, scale :Number = 1) :SwfTexture { @@ -42,50 +46,131 @@ public class SwfTexture const instance :Object = new klass(); const disp :DisplayObject = (instance is BitmapData) ? new Bitmap(BitmapData(instance)) : DisplayObject(instance); + disp.filters = disp.filters.concat(tex.filters); return new SwfTexture(tex.symbol, disp, scale, quality); } public function SwfTexture (symbol :String, disp :DisplayObject, scale :Number, quality :String) { - this.symbol = symbol; - this.scale = scale; - this.quality = quality; - _disp = disp; - - origin = getOrigin(_disp, scale); - - const size :Point = getSize(_disp, scale); - w = size.x; - h = size.y; - a = w * h; + _symbol = symbol; + _quality = quality; + + // wrap object twice for convenience + var wrapper:Sprite = new Sprite(); + wrapper.addChild(disp); + _disp = new Sprite(); + _disp.addChild(wrapper); + + // set the scale and size info + _treatAsFiltered = hasPotentiallySizeAlteringFilters(disp); + setScale(scale); + } + + // scale can be changed after creation, if desired + public function setScale(value:Number):void + { + // To compensate for the fact that filters don't "scale", do the following + // if filtered: + // render at scale 1 + // then scale bitmapData to target size + // if not filtered: + // set the scale in the wrapper + // render directly at target size with vector renderer + if (_treatAsFiltered) { + // cache the scale to use later + _scale = value; + // only need to calculate size once, since _disp is not changing + if (_visualBounds == null) { + recalculateSizeInfo(); + } + } else { + // embed the scale in _disp + var wrapper:DisplayObject = _disp.getChildAt(0); + wrapper.scaleX = wrapper.scaleY = value; + _scale = 1; + // recalculate size since _disp has changed + recalculateSizeInfo(); + } } public function toBitmapData (borderPadding :int = 0) :BitmapData { - const bmd :BitmapData = Util.renderToBitmapData(_disp, w, h, quality, scale); + // render with vector renderer + var bmd :BitmapData = new BitmapData(Math.ceil(_w), Math.ceil(_h), true, 0x00); + var m :Matrix = new Matrix(); + m.translate(_origin.x, _origin.y); + bmd.drawWithQuality(_disp, m, null, null, null, true, _quality); + + // scale bitmap to target size if necessary (only used if _disp contains filters) + if (_scale != 1.0) { + bmd = Util.renderToBitmapData(bmd, this.w, this.h, _quality, _scale); + } + + // add padding if necessary return (borderPadding > 0 ? Util.padBitmapBorder(bmd, borderPadding) : bmd); } - public function toString () :String { return "a " + a + " w " + w + " h " + h; } - - protected static function getSize (disp :DisplayObject, scale :Number) :Point { - const bounds :Rectangle = getBounds(disp, scale); - return new Point(Math.ceil(bounds.width), Math.ceil(bounds.height)); - } - - protected static function getOrigin (disp :DisplayObject, scale :Number) :Point { - const bounds :Rectangle = getBounds(disp, scale); - return new Point(-bounds.x, -bounds.y); + public function toString () :String { return "a " + this.a + " w " + this.w + " h " + this.h; } + + private function recalculateSizeInfo() :void { + // get normal bounds + _strictBounds = _disp.getChildAt(0).getBounds(_disp); + _visualBounds = _strictBounds; + + // possibly increase the visual bounds (due to filter action) + if (_treatAsFiltered) { + // render to bmd + var topLeft:Point = new Point(_s_filteredBmd.width / 2 - _strictBounds.width / 2 - _strictBounds.x, _s_filteredBmd.height / 2 - _strictBounds.height / 2 - _strictBounds.y); + var m :Matrix = new Matrix(1,0,0,1, topLeft.x, topLeft.y); + _s_filteredBmd.drawWithQuality(_disp, m, null, null, null, true, _quality); + + // calculate visual bounds + _visualBounds = _s_filteredBmd.getColorBoundsRect(0xff000000, 0x00000000, false); + _s_filteredBmd.fillRect(_visualBounds, 0x0); + + // adjust registration point + _visualBounds.x = -(topLeft.x - _visualBounds.x); + _visualBounds.y = -(topLeft.y - _visualBounds.y); + } + + // calculate derivative info + _origin = new Point(-_visualBounds.x, -_visualBounds.y); + _w = _visualBounds.width; + _h = _visualBounds.height; } - - protected static function getBounds (disp :DisplayObject, scale :Number) :Rectangle { - const oldScale :Number = disp.scaleX; - disp.scaleX = disp.scaleY = scale; - const holder :Sprite = new Sprite(); - holder.addChild(disp); - const bounds :Rectangle = disp.getBounds(holder); - disp.scaleX = disp.scaleY = oldScale; - return bounds; + + private function hasPotentiallySizeAlteringFilters(dObj:DisplayObject) :Boolean { + // check dObj's filter list + var filters:Array = dObj.filters; + for (var ff:int = 0, nf:int = filters.length; ff < nf; ++ff) { + // all standard filters except ColorMatrixFilter can change the visual bounds + if (!(filters[ff] is ColorMatrixFilter)) { + return true; + } + } + // recursively check children + var dObjContainer:flash.display.DisplayObjectContainer = dObj as flash.display.DisplayObjectContainer; + if (dObjContainer) { + for (var cc:int = 0, nc:int = dObjContainer.numChildren; cc < nc; ++cc) { + var child:DisplayObject = dObjContainer.getChildAt(cc); + if (hasPotentiallySizeAlteringFilters(child)) { + return true; + } + } + } + // all clear + return false; } - - protected var _disp :DisplayObject; + + private var _symbol :String; + private var _disp :DisplayObjectContainer; + private var _w :int, _h :int; + private var _origin :Point; + private var _strictBounds :Rectangle; + private var _visualBounds :Rectangle; + private var _scale :Number; + private var _quality :String; + private var _treatAsFiltered:Boolean; + + static private var _s_filteredBmd:BitmapData = new BitmapData(2048, 2048, true, 0x0); + { _s_filteredBmd.lock(); } } } diff --git a/exporter/src/main/as/flump/xfl/XflKeyframe.as b/exporter/src/main/as/flump/xfl/XflKeyframe.as index 12ef881..82e8260 100644 --- a/exporter/src/main/as/flump/xfl/XflKeyframe.as +++ b/exporter/src/main/as/flump/xfl/XflKeyframe.as @@ -3,7 +3,15 @@ package flump.xfl { +import fl.motion.AdjustColor; +import flash.filters.BevelFilter; +import flash.filters.BitmapFilter; +import flash.filters.BlurFilter; +import flash.filters.ColorMatrixFilter; +import flash.filters.DropShadowFilter; +import flash.filters.GlowFilter; import flash.geom.Matrix; +import flash.utils.Dictionary; import flump.mold.KeyframeMold; @@ -106,7 +114,110 @@ public class XflKeyframe kf.alpha = XmlUtil.getNumberAttr(colorXml, "alphaMultiplier", 1); } } + + parseFilters(lib, location, kf, symbolXml); + return kf; } + + // Parse filters for this symbol+keyframe, store them in a static lookup table + protected static function parseFilters (lib :XflLibrary, location :String, kf :KeyframeMold, symbolXml :XML) :void { + // if filter list is empty, early out + if (symbolXml.filters == null) { + return; + } + + // initialize lookup table entry + _s_filtersByKeyframe[kf] = []; + + // for each filter nodes, parse the xml and add a BitmapFilter to the lookup table + var filter:BitmapFilter; + for each (var filterXml:XML in symbolXml.filters.elements()) { + // parse different filter types into their native flash.filter types + if (filterXml.name().localName == "AdjustColorFilter") { + // + var colorFilter:AdjustColor = new AdjustColor(); + colorFilter.hue = XmlUtil.getNumberAttr(filterXml, "hue", 0); + colorFilter.saturation = XmlUtil.getNumberAttr(filterXml, "saturation", 0); + colorFilter.brightness = XmlUtil.getNumberAttr(filterXml, "brightness", 0); + colorFilter.contrast = XmlUtil.getNumberAttr(filterXml, "contrast", 0); + var mMatrix:Array = colorFilter.CalculateFinalFlatArray(); + filter = new ColorMatrixFilter(mMatrix); + _s_filtersByKeyframe[kf].push(filter); + } else if (filterXml.name().localName == "BlurFilter") { + // + filter = new BlurFilter( + XmlUtil.getNumberAttr(filterXml, "blurX", 5), + XmlUtil.getNumberAttr(filterXml, "blurY", 5), + XmlUtil.getNumberAttr(filterXml, "quality", 1) + ); + _s_filtersByKeyframe[kf].push(filter); + } else if (filterXml.name().localName == "BevelFilter") { + // + filter = new BevelFilter( + XmlUtil.getNumberAttr(filterXml, "distance", 5.0), + XmlUtil.getNumberAttr(filterXml, "angle", 45), + parseInt(XmlUtil.getStringAttr(filterXml, "highlightColor", "#ffffff").substr(1), 16), + XmlUtil.getNumberAttr(filterXml, "highlightAlpha", 1.0), + parseInt(XmlUtil.getStringAttr(filterXml, "shadowColor", "#000000").substr(1), 16), + XmlUtil.getNumberAttr(filterXml, "shadowAlpha", 1.0), + XmlUtil.getNumberAttr(filterXml, "blurX", 5), + XmlUtil.getNumberAttr(filterXml, "blurY", 5), + XmlUtil.getNumberAttr(filterXml, "strength", 1), + XmlUtil.getNumberAttr(filterXml, "quality", 1), + XmlUtil.getStringAttr(filterXml, "type", "inner"), + XmlUtil.getBooleanAttr(filterXml, "knockout", false) + ); + _s_filtersByKeyframe[kf].push(filter); + } else if (filterXml.name().localName == "DropShadowFilter") { + // + filter = new DropShadowFilter( + XmlUtil.getNumberAttr(filterXml, "distance", 5.0), + XmlUtil.getNumberAttr(filterXml, "angle", 45), + parseInt(XmlUtil.getStringAttr(filterXml, "color", "#000000").substr(1), 16), + XmlUtil.getNumberAttr(filterXml, "alpha", 1.0), + XmlUtil.getNumberAttr(filterXml, "blurX", 5), + XmlUtil.getNumberAttr(filterXml, "blurY", 5), + XmlUtil.getNumberAttr(filterXml, "strength", 1), + XmlUtil.getNumberAttr(filterXml, "quality", 1), + XmlUtil.getBooleanAttr(filterXml, "inner", false), + XmlUtil.getBooleanAttr(filterXml, "knockout", false), + XmlUtil.getBooleanAttr(filterXml, "hideObject", false) + ); + _s_filtersByKeyframe[kf].push(filter); + } else if (filterXml.name().localName == "GlowFilter") { + // + filter = new GlowFilter( + parseInt(XmlUtil.getStringAttr(filterXml, "color", "#ff0000").substr(1), 16), + XmlUtil.getNumberAttr(filterXml, "alpha", 1), + XmlUtil.getNumberAttr(filterXml, "blurX", 5), + XmlUtil.getNumberAttr(filterXml, "blurY", 5), + XmlUtil.getNumberAttr(filterXml, "strength", 1), + XmlUtil.getNumberAttr(filterXml, "quality", 1), + XmlUtil.getBooleanAttr(filterXml, "inner", false), + XmlUtil.getBooleanAttr(filterXml, "knockout", false) + ); + _s_filtersByKeyframe[kf].push(filter); + } else { + // parsing for this filter type is unimplemented + lib.addError(location, ParseError.WARN, "Unimplemented parsing for filter type: '" + filterXml.name().localName + "'"); + } + + // if we only parsed unsupported filter types, remove the lookup table entry + if (_s_filtersByKeyframe[kf].length == 0) { + delete _s_filtersByKeyframe[kf]; + } + } + } + + // lookup table for filters associated with a given KeyframeMold + static private var _s_filtersByKeyframe:Dictionary = new Dictionary(true); + static public function getFiltersForKeyframe(kf:KeyframeMold):Array + { + return (kf in _s_filtersByKeyframe) ? _s_filtersByKeyframe[kf] : null; + } + } } diff --git a/exporter/src/main/as/flump/xfl/XflLibrary.as b/exporter/src/main/as/flump/xfl/XflLibrary.as index 8954e48..74765e8 100644 --- a/exporter/src/main/as/flump/xfl/XflLibrary.as +++ b/exporter/src/main/as/flump/xfl/XflLibrary.as @@ -87,7 +87,87 @@ public class XflLibrary } } - for each (movie in movies) if (isExported(movie)) prepareForPublishing(movie); + resolveKfRefs(); + var sortedMovies:Vector. = getSortedMovies(); + for each (movie in sortedMovies) { + if (isExported(movie)) { + propagateFilters(movie, []); + prepareForPublishing(movie); + } + } + } + + protected function resolveKfRefs():void { + for (var ii:int = 0; ii < movies.length; ++ii) { + var movie:MovieMold = movies[ii]; + if (movie.flipbook) + continue; + for each (var layer:LayerMold in movie.layers) { + for each (var kf:KeyframeMold in layer.keyframes) { + if (kf.ref == null) + continue; + kf.ref = _libraryNameToId.get(kf.ref); + } + } + } + } + + // Get movies sorted such that contained movies are after the movies that contain them + protected function getSortedMovies():Vector. { + var sortedMovies:Vector. = movies.concat(); + for (var ii:int = 0; ii < sortedMovies.length; ++ii) { + var movie:MovieMold = sortedMovies[ii]; + for each (var layer:LayerMold in movie.layers) { + for each (var kf:KeyframeMold in layer.keyframes) { + if (kf.ref == null) + continue; + var item:Object = _idToItem[kf.ref]; + if (item is MovieMold) { + var movieToMove:MovieMold = item as MovieMold; + var arrayIndex:int = sortedMovies.indexOf(movieToMove); + if (arrayIndex < ii) { + sortedMovies.splice(arrayIndex, 1); + ii--; + } + sortedMovies.splice(ii + 1, 0, movieToMove); + } + } + } + } + + return sortedMovies; + } + + // Propagate filters down the display graph from a movie and attach them to leaf nodes + protected function propagateFilters(movie:MovieMold, inFilters:Array):void { + for each (var layer:LayerMold in movie.layers) { + for each (var kf:KeyframeMold in layer.keyframes) { + var kfFilters:Array = XflKeyframe.getFiltersForKeyframe(kf); + var filters:Array = kfFilters ? inFilters.concat(kfFilters) : inFilters; + var swfTexture:SwfTexture = null; + if (movie.flipbook) { + // Nothing to be done + } else { + if (kf.ref == null) + continue; + var item:Object = _idToItem[kf.ref]; + if (item == null){ + // Nothing to be done + } else if (item is MovieMold) { + // Propagate filters to component movie + propagateFilters(MovieMold(item), filters); + } else if (item is XflTexture) { + // Assign filters to this XflTexture + const tex:XflTexture = XflTexture(item); + // If filters have previously been assigned, + // replace them only if new list is longer + if (filters.length > tex.filters.length) { + tex.filters = filters; + } + } + } + } + } } protected function prepareForPublishing (movie :MovieMold) :void { @@ -105,7 +185,6 @@ public class XflLibrary } else { if (kf.ref == null) continue; - kf.ref = _libraryNameToId.get(kf.ref); var item :Object = _idToItem[kf.ref]; if (item == null) { addTopLevelError(ParseError.CRIT, diff --git a/exporter/src/main/as/flump/xfl/XflTexture.as b/exporter/src/main/as/flump/xfl/XflTexture.as index 820580a..8db83f9 100644 --- a/exporter/src/main/as/flump/xfl/XflTexture.as +++ b/exporter/src/main/as/flump/xfl/XflTexture.as @@ -11,6 +11,7 @@ import com.threerings.util.XmlUtil; public class XflTexture { public var symbol :String; + public var filters :Array = []; public function XflTexture (lib :XflLibrary, location :String, xml :XML) { symbol = XmlUtil.getStringAttr(xml, "linkageClassName"); @@ -21,5 +22,6 @@ public class XflTexture var swfTex :SwfTexture = SwfTexture.fromTexture(swf, this); return (swfTex.w > 0 && swfTex.h > 0); } + } }