package Packer { import flash.display.MovieClip; import flash.display.Stage; import flash.geom.*; import flash.display.*; import flash.utils.ByteArray; import flash.utils.getQualifiedClassName; import flash.net.FileReference; import flash.utils.Endian; import flash.events.MouseEvent; import flash.events.KeyboardEvent; import flash.utils.*; import djarts.core.CacheManager; import com.adobe.serialization.json.JSON; import flash.filters.BitmapFilter; import flash.filters.BlurFilter; import flash.filters.ColorMatrixFilter; import fl.motion.AdjustColor; import djarts.core.*; import djarts.display.*; public class GraphicPacker { protected var clips:Array = new Array(); protected var clipSizing:Array = new Array(); protected var fonts:Array = new Array(); protected var fontCopies:Array = new Array(); protected var sheetWidth = 1024; protected var sheetHeight = 1024; protected var stage; protected var additionalData = {}; public var regionsOutput = new MovieClip(); public function Init(sheetWidth, sheetHeight, stage) { this.sheetWidth = sheetWidth; this.sheetHeight = sheetHeight; this.stage = stage; } protected var doneCallback = null; protected var callbackParam; public function ExportDefs(graphicDefs:Array, callback, callbackParam) { this.doneCallback = callback; this.callbackParam = callbackParam; var clipCollection:GraphicDefClipCollection = new GraphicDefClipCollection(graphicDefs, GraphicDefsLoaded); } public function ExportClips(clips:Array) { this.clips = clips; var sheetSize:Point = this.DetermineMinimumSheetSizeForClips(); this.sheetWidth = sheetSize.x; this.sheetHeight = sheetSize.y; return this.Export(false); } public function GraphicDefsLoaded(clips:Array) { this.clips = clips; var sheetSize:Point = this.DetermineMinimumSheetSizeForClips(); this.sheetWidth = sheetSize.x; this.sheetHeight = sheetSize.y; var output:ByteArray = this.Export(false); this.doneCallback(output, callbackParam); } public function AddClip(clip:MovieClip, scale=1, sizing:Point = null) { clip.scaleX = clip.scaleX * scale; clip.scaleY = clip.scaleY * scale; clips.push(clip); clipSizing.push(sizing); } public function AddBitmapFont(font:BitmapFont) { fonts.push(font); } public function AddBitmapFontCopy(font:BitmapFont, toName:String) { fontCopies.push({from:font,to:toName}); } protected var sheet = 0; public function ExportFrom(clip, scale=1, save = true) { for (var i = 0; i < clip.numChildren; i++) { if (clip.getChildAt(i) is MovieClip) { AddClip(clip.getChildAt(i), scale); } } return Export(save); } public static const CACHESEQUENCES = 0; public static const PAD = 2; public function GetSheets(){return sheets;} public var sheets:Array = new Array(); protected var sprites:Array = new Array(); protected var spriteOriginalClips:Array = new Array(); public function Export(save = true) { additionalData = {}; sheets = new Array(); sprites = new Array(); spriteOriginalClips = new Array(); var areas:Array = new Array(); for (var i in clips) { areas[i] = clips[i].width * clips[i].height; } var sorted:Array = areas.sort(Array.RETURNINDEXEDARRAY | Array.NUMERIC | Array.DESCENDING); sheets.push(new SpriteSheet(sheetWidth, sheetHeight)); for (var i = 0; i < clips.length; i++) { var sprite:TextureSprite = new TextureSprite(clips[sorted[i]], CACHESEQUENCES); CacheClip(clips[sorted[i]], sprite); sprites.push(sprite); spriteOriginalClips.push(clips[sorted[i]]); _strace("cached clip "+i+" of "+clips.length); } var fontSpritesByName = {}; for (var i in fonts) { var sprite:TextureSprite = new TextureSprite(fonts[i], CACHESEQUENCES); CopyBitmapFontToSingleSheet(fonts[i], sprite); sprites.push(sprite); spriteOriginalClips.push(fonts[i]); additionalData[fonts[i].Name] = fonts[i].GetAdditionalData(); fontSpritesByName[fonts[i].Name] = sprite; } for (var i in fontCopies) { var f:BitmapFontCopy = new BitmapFontCopy(fontCopies[i].to, fontCopies[i].from.Size, fontCopies[i].from.Bold, fontCopies[i].from); sprites.push(fontSpritesByName[fontCopies[i].from.Name]); spriteOriginalClips.push(f); additionalData[f.Name] = f.GetAdditionalData(); } var output:ByteArray = new ByteArray(); output.endian = Endian.LITTLE_ENDIAN; output.writeUnsignedInt(sheets.length); for (var i = 0; i < sheets.length; i++) { var s:SpriteSheet = sheets[i]; output.writeUnsignedInt(s.width); output.writeUnsignedInt(s.height); var png:ByteArray = PNGExportHelper.GetExportPNG(s.GetBitmap()); output.writeUnsignedInt(png.length); output.writeBytes(png); } for (var i = 0; i < sprites.length; i++) { var orig = spriteOriginalClips[i]; if (orig is BitmapFont && orig.AsBackupOnly) { sprites.splice(i, 1); spriteOriginalClips.splice(i, 1); i--; } } output.writeUnsignedInt(sprites.length); for (var i = 0; i < sprites.length; i++) { var tex:TextureSprite = sprites[i]; var orig = spriteOriginalClips[i]; var clipName = "clip"; if (orig is BitmapFont || orig is BitmapFontCopy) { clipName = orig.Name; } else { clipName = getQualifiedClassName(orig); if (clipName == "flash.display::MovieClip") { clipName = orig.name; } } _strace("Exporting "+clipName); output.writeUnsignedInt(clipName.length); output.writeMultiByte(clipName, "us-ascii"); output.writeUnsignedInt(tex.NumSequences()); for (var j = 0; j < tex.NumSequences(); j++) { var info:SequenceInfo = tex.GetSequenceInfo(j); output.writeUnsignedInt(info.Length); _strace("With "+info.Length+" Frames"); for (var k = 0; k < info.Length; k++) { output.writeUnsignedInt(sheets.indexOf(info.FrameTextures[k])); var location:Rectangle = info.FrameNodes[k].Rect; output.writeFloat(location.x + PAD); output.writeFloat(location.y + PAD); output.writeFloat(location.width - (PAD * 2)); output.writeFloat(location.height - (PAD * 2)); var offset:Point = info.FrameOffsets[k]; output.writeFloat(offset.x - PAD); output.writeFloat(offset.y - PAD); regionsOutput.graphics.beginFill(0x00CCFF, .2); regionsOutput.graphics.lineStyle(1, 0x00CCFF); regionsOutput.graphics.drawRect(location.x + PAD, location.y + PAD, location.width - (PAD * 2), location.height - (PAD * 2)); regionsOutput.graphics.endFill(); } } } var additionalDataString = com.adobe.serialization.json.JSON.encode(additionalData); output.writeUnsignedInt(additionalDataString.length); output.writeMultiByte(additionalDataString, "us-ascii"); if (save) { var fr:FileReference = new FileReference(); fr.save(output, "data.pak"); } _strace("done. num sheets: " + sheets.length); return output; } public function DetermineMinimumSheetSizeForClips():Point { /* TODO - FOR LARGE SIZED CHUNKS BETTER HANDLING FOR FINDING MINIMUM SIZE! */ sheets = new Array(); sprites = new Array(); var areas:Array = new Array(); for (var i in clips) { areas[i] = clips[i].width * clips[i].height; } var sorted:Array = areas.sort(Array.RETURNINDEXEDARRAY | Array.NUMERIC | Array.DESCENDING); sheets.push(new SpriteSheet(2048, 2048)); for (var i = 0; i < clips.length; i++) { var sprite:TextureSprite = new TextureSprite(clips[sorted[i]], CACHESEQUENCES); CacheClip(clips[sorted[i]], sprite); sprites.push(sprite); } var allUsedRects:Array = new Array(); var nodesToVisit:Array = new Array(); for (var i = 0; i < sheets.length; i++) { var sheet:SpriteSheet = sheets[i]; var nodes = sheet.GetAllocator().GetLeafNodes(); for (var n = 0; n < nodes.length; n++) { allUsedRects.push(nodes[n].Rect); } } var sizeFound = false; var sheetSizeX = 64; var sheetSizeY = 64; while(!sizeFound) { var sheet:SpriteSheet = new SpriteSheet(sheetSizeX, sheetSizeY); sizeFound = true; for(var i = 0; i < allUsedRects.length; i++) { var node:RectangleNode = sheet.GetAllocator().Insert(allUsedRects[i].width, allUsedRects[i].height); if (node == null) { sizeFound = false; break; } } if (sizeFound == false) { if (sheetSizeX == sheetSizeY) { sheetSizeX *= 2; } else { sheetSizeY *= 2; } } } return new Point(sheetSizeX, sheetSizeY); } protected function CopyBitmapFontToSingleSheet(font:BitmapFont, sprite:TextureSprite) { sprite.SetSequence(0, font.CodepointData.length); // info to go into the sprite var offsets:Array = new Array(); var bounds:Array = new Array(); var nodes:Array = new Array(); var localSheets:Array = new Array(); // track the nodes we have locally created, if we can't fit // everything on 1 sheet we will remove them var createdNodes:Array = new Array(); var createdNewSheet = false; var useSheets:Array = new Array(); for (var s = 0; s < this.sheets.length; s++) { useSheets.push(s); } for (var sheetIndex = 0; sheetIndex < useSheets.length; sheetIndex++) { var s = useSheets[sheetIndex]; var sheet:SpriteSheet = sheets[s]; if (!sheet.sheetFull) { var ok = true; for (var i = 0; i < font.CodepointData.length; i++) { var node:RectangleNode = null; var data:FontCodepointData = font.CodepointData[i]; if (font.AsBackupOnly && data.References == 0) { // do nothing } else if (data.FromOtherFont) { // this is a copy node, from a backup font node = data.FromFont.CodepointData[data.FromFontIndex].DestinationNode; } else { node = sheet.GetAllocator().Insert(data.Data.width, data.Data.height); if (node == null) { // failed to fit the entire font on a sheet, so let's clear // everything and start again ok = false; for (var j in createdNodes) { sheet.GetBitmap().fillRect(createdNodes[j].Rect, 0x0); } sheet.GetAllocator().RemoveNodes(createdNodes, false); nodes = new Array(); createdNodes = new Array(); break; } createdNodes.push(node); } nodes.push(node); } if (ok) { // the nodes array is completed (length == font.CodepointData.length) // now we have to setup the sheet indices, bounds, offsets, etc... for (var i = 0; i < font.CodepointData.length; i++) { var data:FontCodepointData = font.CodepointData[i]; if (font.AsBackupOnly && data.References == 0) { // do nothing offsets.push(new Point()); bounds.push(new Rectangle()); localSheets.push(-1); } else { var node:RectangleNode = nodes[i]; if (!data.FromOtherFont) { sheet.GetBitmap().copyPixels(data.Data, data.Data.rect, new Point(node.Rect.x, node.Rect.y), null, null, true); } offsets.push(data.Offset); bounds.push(node.Rect.clone()); localSheets.push(node.Host.HostTexture); data.DestinationNode = node; } } break; } else { if (s == sheets.length - 1) { if (createdNewSheet) { // couldn't fit the font on 1 sheet CopyBitmapFontToSheets(font, sprite); return; } else { createdNewSheet = true; var newSheet:SpriteSheet = new SpriteSheet(sheetWidth, sheetHeight); newSheet.SheetIndex = sheets.length; useSheets.push(sheets.length); sheets.push(newSheet); } } } } } for (var j = 0; j < nodes.length; j++) { sprite.AddFrame(0, j, offsets[j], bounds[j], localSheets[j], nodes[j], null); } sprite.Loaded = true; } protected function CopyBitmapFontToSheets(font:BitmapFont, sprite:TextureSprite) { sprite.SetSequence(0, font.CodepointData.length); // info to go into the sprite var offsets:Array = new Array(); var bounds:Array = new Array(); var nodes:Array = new Array(); var localSheets:Array = new Array(); var i = 0; var useSheets:Array = new Array(); for (var s = 0; s < this.sheets.length; s++) { useSheets.push(s); } var sheetIndex = useSheets.length - 1; var searchBackwards = true; var done = false; while (!done) { var s = useSheets[sheetIndex]; var sheet:SpriteSheet = sheets[s]; if (!sheet.sheetFull) { var count = 0; var spaceAvailable = true; while (spaceAvailable && i < font.CodepointData.length) { var data:FontCodepointData = font.CodepointData[i]; var node:RectangleNode = null; var offset:Point = new Point(); if (font.AsBackupOnly && data.References == 0) { // do nothing, this sprite won't be used ever } else if (data.FromOtherFont) { var copyData:FontCodepointData = data.FromFont.CodepointData[data.FromFontIndex]; node = copyData.DestinationNode; offset = copyData.Offset; } else { node = sheet.GetAllocator().Insert(data.Data.width, data.Data.height); if (node == null) { spaceAvailable = false; break; } offset = data.Offset; sheet.GetBitmap().copyPixels(data.Data, data.Data.rect, new Point(node.Rect.x, node.Rect.y), null, null, true); } nodes.push(node); offsets.push(offset); bounds.push(node.Rect.clone()); localSheets.push(node.Host.HostTexture); data.DestinationNode = node; i++; count++; } /* if (count > 0) { for (var q = 0; q < count; q++) { localSheets.push(sheet); } } */ if (i < font.CodepointData.length) { if (searchBackwards) { sheetIndex--; if (sheetIndex < 0) { searchBackwards = false; sheetIndex = sheets.length - 1; } } if (!searchBackwards) { var newSheet:SpriteSheet = new SpriteSheet(sheetWidth, sheetHeight); newSheet.SheetIndex = sheets.length; useSheets.push(sheets.length); sheets.push(newSheet); sheetIndex++; } } else { done = true; } } } for (var j = 0; j < offsets.length; j++) { sprite.AddFrame(0, j, offsets[j], bounds[j], localSheets[j], nodes[j], null); } sprite.Loaded = true; } public static var UseFlatHeuristic = false; private function AnalyzeClipType(clip:MovieClip):MovieClip { var isFlat = false; if (clip.totalFrames > 1 && UseFlatHeuristic) { var framesWithMultiChildren = 0; var totalChildren = 0; var childrenWithSingleFrame = 0; for (var i = 0; i < clip.totalFrames; i++) { clip.gotoAndStop(i); var children = clip.numChildren; totalChildren += children; if (children > 1) framesWithMultiChildren++; for (var j = 0; j < clip.numChildren; j++) { var child = clip.getChildAt(j); if (!(child is MovieClip) || child.totalFrames == 1) { childrenWithSingleFrame++; } } } if (framesWithMultiChildren / clip.totalFrames > 0.75) { if (childrenWithSingleFrame / totalChildren > 0.75) { isFlat = true; } } } if (isFlat) { var ret:MovieClip = new MovieClip(); ret.addChild(clip); return ret; } else { return clip; } } protected function CacheClip(clip:MovieClip, sprite:TextureSprite) { clip = AnalyzeClipType(clip); var totalFrames = clip.totalFrames; var startSeq = 1; var endSeq = totalFrames; for (var i = startSeq; i <= endSeq; i++) { var endFrame = 1; RecursivelyStop(clip, i); for (var j = 0; j < clip.numChildren; j++) { var child = clip.getChildAt(j); if (child is MovieClip) { if (child.totalFrames > endFrame) endFrame = child.totalFrames; } } var offsets:Array = new Array(); var bounds:Array = new Array(); var nodes:Array = new Array(); var localSheets:Array = new Array(); var completed = false; var startFrame = 1; sprite.SetSequence(i - 1, endFrame); var origStart = startFrame; var useSheets:Array = new Array(); for (var s = 0; s < this.sheets.length; s++) { useSheets.push(s); } for (var sheetIndex = 0; sheetIndex < useSheets.length; sheetIndex++) { var s = useSheets[sheetIndex]; var sheet:SpriteSheet = sheets[s]; //if (this.ForceSpriteSheet) sheet = this.ForceSpriteSheet; if (!sheet.sheetFull) { var count = 0; count = CacheFrames(clip, offsets, bounds, sheet, nodes, startFrame, endFrame); if (count > 0) { for (var q = 0; q < count; q++) { localSheets.push(sheet); } } startFrame += count; if (startFrame <= endFrame && s == sheets.length - 1) { var newSheet:SpriteSheet = new SpriteSheet(sheetWidth, sheetHeight); newSheet.SheetIndex = sheets.length; useSheets.push(sheets.length); sheets.push(newSheet); } } } if (localSheets.length < endFrame - startFrame + 1) { _strace("GraphicPacker: Problem caching"); } for (var j = 0; j < offsets.length; j++) { sprite.AddFrame(i - 1, j + origStart - 1, offsets[j], bounds[j], localSheets[j], nodes[j], null); } } sprite.Loaded = true; } protected function CacheFrames(clip:MovieClip, offsets:Array, bounds:Array, sheet:SpriteSheet, nodes:Array, startFrame = 1, lastFrame = -1) { if (lastFrame == -1) lastFrame = clip.totalFrames; for (var i = startFrame; i <= lastFrame; i++) { SubclipsGotoFrame(clip, i, {}, clip); RecursivelyStop(clip); var currentMatrix = clip.transform.matrix.clone(); var flippedX = currentMatrix.a < 0; var flippedY = currentMatrix.d < 0; var m:Matrix = new Matrix(currentMatrix.a, 0, 0, currentMatrix.d); var r:Rectangle = clip.getBounds(clip); r = CacheManager.Instance().GetAccurateBounds(clip, 2048); var usedCacheBounds = false; if (clip.getChildByName("__cache_bounds__")) { usedCacheBounds = true; r = clip.getChildByName("__cache_bounds__").getRect(clip); clip.mask = clip.getChildByName("__cache_bounds__"); } r.x *= Math.abs(m.a); r.y *= Math.abs(m.d); r.width *= Math.abs(m.a); r.height *= Math.abs(m.d); r.width += PAD * 2.0; r.height += PAD * 2.0; var w = Math.ceil(r.width); var h = Math.ceil(r.height); var node:RectangleNode = sheet.GetAllocator().Insert(w, h); if (node == null) { return i - startFrame; } nodes.push(node); var target:Point = new Point(node.Rect.x, node.Rect.y); //flippedX = false; var offsetX = flippedX ? Math.ceil(r.right - PAD * 2.0) : Math.ceil(-r.left); var offsetY = flippedY ? Math.ceil(r.bottom - PAD * 2.0) : Math.ceil(-r.top); m.translate(offsetX + target.x + PAD, offsetY + target.y + PAD); var offset:Point = new Point(offsetX + PAD, offsetY + PAD); RenderBitmap(sheet, clip, m, new ColorTransform(1.0, 1.0, 1.0, clip.alpha)); offsets.push(offset); bounds.push(node.Rect.clone()); if (usedCacheBounds) { clip.mask = null; } } return lastFrame - startFrame + 1; } protected function RenderBitmap(sheet:SpriteSheet, clip:MovieClip, m:Matrix, ct:ColorTransform) { sheet.GetBitmap().draw(clip, m, ct); } protected function SubclipsGotoFrame(clip:MovieClip, i, force, rootClip, exceptClip = null) { for (var j = 0; j < clip.numChildren; j++) { var child = clip.getChildAt(j); if (child is MovieClip && child != exceptClip) { if (i != null) child.gotoAndStop(((i - 1) % child.totalFrames) + 1); SubclipsGotoFrame(child, i, force, rootClip); } } } protected function RecursivelyStop(clip, frame = null) { if (clip is MovieClip) { if (frame == null) { clip.stop(); } else { clip.gotoAndStop(frame); } } if (clip is DisplayObjectContainer) { for (var i = 0; i < clip.numChildren; i++) { RecursivelyStop(clip.getChildAt(i)); } } } } }