Here’s one of those things that we just sort of needed to do. Neal (nealio) is working on a project where we need several PNGs as external assets to be loaded into Flash. And these PNGs would work better as SWFs, where we can apply JPEG compression but maintain the alpha channel.
We’ve used, in the past, a Fireworks extension to batch process PNGs as SWFs, and it’s worked well, except that Fireworks, even CS4, can only export AVM1 SWFs. We’re forward thinkers. We don’t like that. How to batch process PNGs to AVM2 SWFs, then?
By rolling our own. I whipped up this JSFL script in about 20 minutes, and then spent a little time testing and adding a few niceties. In the end, it’s pretty simple but uses Flash as the batch tool, so that we can export for Flash 10/ActionScript 3. Here’s the code:
fl.outputPanel.clear();
var quality = prompt("Desired JPEG Quality:")
var smoothing;
var fla;
if (quality) {
smoothing = confirm("Smoothing on?");
quality = parseInt(quality);
var folder = fl.browseForFolderURL("Choose a folder of images to process");
if (folder) {
fl.trace("STARTING BATCH PROCESS")
fl.trace("JPEG Quality: " + quality);
fl.trace("Smoothing: " + smoothing);
fl.trace("Base folder: " + folder + "\n");
fl.trace("FILES PROCESSED\n");
start(folder);
}
}
/**
* Kicks things off. Creates a new Flash document for PNG-import/SWF-creation purposes. Then processes
* the folder, and finally closes the Flash document.
* @param folder URI of the folder to process.
**/
function start(folder) {
fla = fl.createDocument();
processFolder(folder, 0);
fla.close(false);
}
/**
* Takes a folder and iterates over the files in it. Iterates recursively through sub-folers. Only processes
* files ending in .png, .jpg, .jpeg, or .gif.
* @param folder URI of the folder to process
* @param level An index of how many levels "deep" we are through the recursion. Used for formatting
* the output as files get processed.
**/
function processFolder(folder, level) {
var files = FLfile.listFolder(folder, "files");
var folders = FLfile.listFolder(folder, "directories");
// Loop over the files, only processing
var iLen = files.length;
for (var i = 0; i < iLen; i++) {
var file = files[i];
if (file.match(/\.(png|jpg|jpeg|gif)$/)) {
fl.trace(tab(level) + file)
createSWF(file, folder);
}
}
// Recurse through the folders in this folders.
iLen = folders.length;
for (i = 0; i < iLen; i++) {
fl.trace(tab(level) + folders[i] + "/")
processFolder(folder + "/" + folders[i], level + 1);
}
}
/**
* Utility function to produce a string with a certain number of tabs in it (used to created an indented tree list
* of files processed).
* @param level How many tabs to produce.
**/
function tab(level) {
var t = ""
for (var i = 0; i < level; i++) {
t += "\t";
}
return t;
}
/**
* Takes an image file in a folder and creates a SWF of the same base name (ending in .swf) in the same folder.
* @param file The file name (not the full file URI) of the image to turn into a SWF
* @param folder The File URI of the folder that the file is in.
**/
function createSWF(file, folder) {
var importURI = folder + "/" + file;
fla.importFile(importURI);
var sel = fl.getDocumentDOM().selection[0];
sel.x = 0;
sel.y = 0;
var lib = sel.libraryItem;
lib.compressionType = "photo";
lib.quality = quality;
lib.allowSmoothing = smoothing;
var filenamePieces = file.split(".");
filenamePieces.pop();
fla.exportSWF(folder + "/" + filenamePieces.join(".") + ".swf");
fla.deleteSelection();
}
You may wonder what’s so special about an image saved in an AVM2 SWF. Well, it turns out that in Flash 10, you can’t reparent an AVM2 SWF. Try this:
var l:Loader = new Loader();
l.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
l.load(new URLRequest("AVM1.swf"));
function onComplete(e:Event):void {
addChild(l.content);
}
And you’ll get the following runtime error:
ArgumentError: Error #2180: It is illegal to move AVM1 content (AS1 or AS2) to a different part of the displayList when it has been loaded into AVM2 (AS3) content.
at flash.display::DisplayObjectContainer/addChild()
at AVM2_fla::MainTimeline/onComplete()
Weirdly, you only get this error if you are publishing for Flash 10. Change your publish settings for Flash 9, and the preceding example will work.
Even if you’re not reparenting the content, it’s probably not a bad idea, for performance reasons, to go AVM2 with all of your SWFs, even for SWFs that merely contain a bitmap. I haven’t done any tests, but I would assume that creating Loaders with AVM1Movie instances in them is inherently slower than Loaders with Bitmaps and MovieClip in them.
I’d love to flesh this out and make it more robust. A single UI would be nice, instead of a series of prompts and stuff. In fact, a whole UI for selecting your files, your output destination, and your output format would be bully (much like the Fireworks extension we used). But this was one of those “If I can’t figure this out in 20 minutes I’m giving up” things. I figured I’d share because it does work, even if it’s a bit primitive. If you have improvements, be it through suggestion or through actual code, let us know in the comments.
The short answer: Yes and no.
Or slightly more accurately, No and yes.
Confused? As was I. On to the long answer (don’t worry, not as long as is usual for me):
The addition of the close method on the Loader class is pretty great. We can now finally stop a load in progress if, for whatever reason, we deem it necessary. Maybe the user changed her mind and clicked another button while one SWF (no longer needed) was loading. Maybe we want to do something tricksy like start loading an asset, but not fully, to get the bytesTotal.
MovieClipLoader in AS2 had unloadClip, and you could interrupt a load in progress by telling it to load a non-existant URL into the same clip…but this is agreeably messy. So hurrah for Loader.close!
Except…if you try it out, it most likely won’t work. Create a simple test FLA with a clip set to be a button, called “btn_mc” and add the following code to the first frame:
var l:Loader = new Loader();
addChildAt(l, 0);
l.load(new URLRequest("path/to/some/large/asset.swf"));
btn_mc.addEventListener(MouseEvent.CLICK, doit);
function doit(me:MouseEvent):void {
trace('doing it');
l.close();
}
Now test the SWF, and then enter bandwidth simulation mode (press Command/Control-Enter again), and while the asset is still loading, click the button. But wait for it…and you’ll see it show up anyway? Wisconsin Tourism Federation? (work out the acronym…)
This is the “no” in the “no and yes” answer. I put the “no” first because this is likely the first thing you’ll try when working with Loader.close. So what about that “yes?” Is there some extra step to take? Some undocumented feature to enable?
No, it’s even simpler than that. You need to load your asset from a server.
Replace the “path/to/some/large/asset.swf” with “http://some.domain/path/to/some/large/asset.swf” and you should be good.
Needless to say, this can be the source of confusion. I don’t know what makes an HTTP stream different from a local file stream, but I concede that there are differences and that that probably accounts for this behavior. But it sure would be great if this behavior were mentioned in the documentation. It would be even better if this method just worked. Why can’t we close a Loader when loading an asset locally? This is how 99% of our development takes place; we almost always use a relative path to the asset, and we almost always develop on our local boxes without worrying about servers initially.
Earl and I just hammered out a weird issue in some code that involved Brownian motion. Check this out, assuming p is a MovieClip on the stage:
addEventListener(Event.ENTER_FRAME, animate);
function animate(e:Event):void {
p.x += Math.random() * 2 - 1;
p.y += Math.random() * 2 - 1;
}
Seems simple enough, right? The particle p should move randomly, and produce what is called “Brownian motion.” It should wander, but ultimately stay within a general area, on average.
However, if you let it run long, you’ll find the particle tending to gravitate to 0, 0. If you’re impatient, you can speed up the process like this:
addEventListener(Event.ENTER_FRAME, animate);
function animate(e:Event):void {
for (var i:int = 0; i < 100; i++) {
p.x += Math.random() * 2 - 1;
p.y += Math.random() * 2 - 1;
}
}
After trying quite a few things, we eventually tried the following:
addEventListener(Event.ENTER_FRAME, animate);
var realX:Number = p.x;
var realY:Number = p.y;
function animate(e:Event):void {
realX += Math.random() * 2 - 1;
realY += Math.random() * 2 - 1;
p.x = realX;
p.y = realY;
}
Again, you can wrap that up in the loop to speed things up.
Either way, you’ll notice that we get proper motion now. What gives?
Well, the suspicion that lead to us trying that was confirmed when we added a trace:
addEventListener(Event.ENTER_FRAME, animate);
var realX:Number = p.x;
var realY:Number = p.y;
function animate(e:Event):void {
realX += Math.random() * 2 - 1;
realY += Math.random() * 2 - 1;
p.x = realX;
p.y = realY;
trace(realX, p.x);
}
You’ll see something like this:
310.60525778401643 310.6
311.387398567982 311.35
312.1899666218087 312.15
311.5306175108999 311.5
311.22865250799805 311.2
311.0907105393708 311.05
310.28716491162777 310.25
309.4788754032925 309.45
309.128194604069 309.1
308.98307433445007 308.95
309.23653392959386 309.2
308.6192215178162 308.6
309.3771039834246 309.35
Check that out! The x property (and the y, too, we checked) is rounded internally to the nearest .05. Actually, it’s not even rounded, it’s floored (my assumption is that it’s just truncating some bits resulting in a floor effect, which would presumably be a faster operation that Math.floor).
So, basically, we’re talking about rounding errors that, when piled up, result in a gradual approach to 0. For example, say you start at 10. You generate your random number, and it’s -1.46. You take x property of 10, subtract 1.46, and you’d expect to end up at 8.54. Instead, you end up 8.5. Confirm it with the following code:
p.x = 10;
p.x += -1.46
trace(p.x);
trace(10 - 1.46)
A similar effect happens with positive numbers…you end up smaller than you think you will. Thus, a gradual approach to 0.
The fix involves tracking a high-precision Number value as the “real” x and y, and adding to that. Then, simply assigning that value to the MovieClip’s x and y still results in rounding, but without any cumulative effects. Anyone else remember the similar problem from ActionScript 2 days with _alpha?
My theory is that since there is a finite precision to Numbers, we’d still see a gradual approach to 0, only it’ll take MUCH longer.
So, I’m not necessarily pointing the finger at Adobe here and saying this is a bug; this is clearly intentional behavior, and 99% of the time you don’t even notice. But when you do have that situation where it matters, you need to know about it.
It may not be obvious until you actually make the mistake and spend half a day trying to find the miscalculation in what you thought was an intuitive use of flash’s built in tools. DisplayObjects have the getBounds method, allowing you to quickly retrieve a Rectangle object containing size and location properties pertaining to that object in relation to any other display object, whether on the stage or not. Passing a reference of a clip into its own getBounds method returns a rectangle complete relative to itself, very handy for quickly retrieving an object’s dimensions.
Be careful, however, because using this method of retrieving an object’s dimensions is very relative. When asked for dimensions relative to itself, a DisplayObject will return unscaled values. So if you’ve rescaled a MovieClip on the stage and plan to quickly pass around its size dimensions, you’ll be better off creating your own Rectangle than relying on getBounds, especially when there’s a possibility that the object is not its original width or height.
Furthermore, it may be dangerous in a dynamic situation to simply getBounds against the object’s parent, and the object’s root property is only valid if the clip is somewhere on the stage. In these situations it may be best to either go through the extra motions of constructing your own rectangle or only use object.getBounds(object) when you’re positive the object is not scaled.
var square:Sprite = new Sprite();
addChild(square);
square.graphics.beginFill(0x0000FF);
square.graphics.drawRect(0,0,100,100);
square.graphics.endFill();
square.x = stage.stageWidth/2 - square.width/2;
square.y = stage.stageHeight/2 - square.height/2;
var b:Rectangle;
b = square.getBounds(this);
trace(b); // traces (x=225, y=150, w=100, h=100)
square.scaleX = 2;
b = square.getBounds(square);
trace(b); // traces (x=0, y=0, w=100, h=100)
So do not expect the second trace in this code block to return a rectangle with a width of 200. Being a completely relative call it will return the object’s independent width within its scaled universe. As I mentioned, it makes sense from an oop standpoint, but its easy to hope or expect a scaled return if you’re needing that returned in a situation.
We’ve often gone about the run-around method to find out how many bytes a particular class or package of Actionscript files adds to a compiled swf. In the pre CS4 days (unless there was something of which we were unawares) we often created a new AS3 fla, imported and declared only the classes to be tested, and then compared the swf filesize to that of an empty swf. Not a terribly friendly method.
I haven’t seen a better method documented anywhere. Though I’ve recently found that Flash CS4 will include a list of compiled classes and their corresponding byte counts when using Generate Size Report. The trick, however, is that you also have to use the publish settings to automatically export a swc.
Here’s an example of a list of compiled classes produced when both “Generate size report” and “Export SWC” are checked:
Of course this could have been expected. A swc is just a zipped up swf and catalog.xml, listing all the compiled classes in the swf. I’m not sure why Flash doesn’t have a better system (and a more obvious reporting system) in the first place, but this hidden perk certainly reinforces the sensical quirkiness of the IDE we’ve all come to love over the years. The more you love the quirks, the more the IDE will love you back.
Here’s a real quick JSFL trick. We all know that working with color transforms in code is cool, except that it’s usually easier to design a transform using the Color: Advanced setting in the Properties panel. Here ya go:
var s = fl.getDocumentDOM().selection[0];
fl.trace("new ColorTransform("
+ (s.colorRedPercent / 100) + ", "
+ (s.colorGreenPercent / 100) + ", "
+ (s.colorBluePercent / 100) + ", "
+ (s.colorAlphaPercent / 100) + ", "
+ (s.colorRedAmount ) + ", "
+ (s.colorGreenAmount) + ", "
+ (s.colorBlueAmount ) + ", "
+ (s.colorAlphaAmount)
+")");
Just select an object and run the script. You’ll get a trace in the Output panel with the equivalent ActionScript ColorTransform. If you like, you can even modify it like so:
fl.trace(s.name + ".transform.colorTransform = new ColorTransform("
If you’ll be applying this ColorTransform to the instance you selected, rather than using the selection as a temporary guide.
Ever make a symbol with the registration point in the, say, default top left corner, and then later realize you need it in the center for rotation or scaling purposes? But you’ve already placed the instance on stage where it needs to be, so not only do you have to move the symbol’s contents, you have to move the whole instance on stage by a complementary amount. Unless you have this script! Check it out:
fl.outputPanel.clear();
function run() {
var e = fl.getDocumentDOM().selection[0];
if (!e) return;
var x = parseFloat(prompt("Enter new x:", e.x));
if (isNaN(x)) return;
var y = parseFloat(prompt("Enter new y:", e.y));
if (isNaN(y)) return;
fl.trace("Moving element from (" + e.x + ", " + e.y + ") to (" + x + ", " + y + ")");
var diffX = e.x - x;
var diffY = e.y - y;
e.x = x;
e.y = y;
// Get the Timeline of the object in question.
var tl = e.libraryItem.timeline;
// Loop over the Timeline's Layers.
var iLen = tl.layers.length;
for (var i = 0; i < iLen; i++) {
var l = tl.layers[i];
fl.trace("Layer: " + l.name);
// Loop over each Layer's Frames.
var jLen = l.frames.length;
for (var j = 0; j < jLen; j++) {
var f = l.frames[j];
// Don't mess with elements in frames that aren't keyframes...
if (f.startFrame != j) continue;
fl.trace("\tFrame: " + (j+1));
// Loop over each keyframe's elements.
var kLen = f.elements.length;
for (var k = 0; k < kLen; k++) {
// Move the element.
var child = f.elements[k];
fl.trace("\t\t" + child + " " + k + " :: " + child.name
+ "("+child.x+","+child.y+") => ("+(child.x+diffX)+","+(child.y+diffY)+")");
child.x += diffX;
child.y += diffY;
}
}
}
}
run();
Select an item (one item at a time, please, although it shouldn’t be hard to expand this to loop over every item in the selection, assuming you want to move a bunch of items by the same amount). You’ll be prompted for a new x and a new y. This is the x,y of the registration point of the instance. Plugging in new values that are, say, 100 pixels greater than the current values will move the symbol’s contents up and to the left by 100 pixels, while moving the instance down and to the right by 100 pixels. It even traverses the keyframes of each layer, so a timeline animation should get adjusted all at once.
A log of what happened will show up in your Output panel:
Moving element from (116, 148) to (216, 248)
Layer: Layer 3
Frame: 1
[object SymbolInstance] 0 :: (53.7,42.7) => (-46.3,-57.3)
Layer: Layer 2
Frame: 1
[object Shape] 0 :: (43.45,85.45) => (-56.55,-14.549999999999997)
Layer: Layer 1
Frame: 1
[object Shape] 0 :: (100.95,38.95) => (0.9500000000000028,-61.05)
It’s worked reasonably well for me so far, but this is one of those things where the logic is complicated enough to possibly fail in certain situations. If you run into such a situation, please let us know in the comments. I’d love to improve this script if it needs it.
We’ll just make this JSFL kick a mini-series. Here’s another handy script. I think I originally wrote this while working in Flash CS3, because Flash CS4 provides a “search in library” field. Even so, this can save a few steps. Simply select an instance on the stage, and run the script, and the instance’s symbol will be selected in the Library. One failing is that if the Library is not already showing, the script can’t do anything to open it. The item does get selected, though. You just need to open the Library manually.
// Get the objects we'll need to work with.
var doc = fl.getDocumentDOM();
var sel = doc.selection[0];
var lib = doc.library;
// Grab the name of the LibraryItem object of the current selection.
var libItem = sel.libraryItem.name;
// Take that name and split it into "crumbs." If we have more than one
// crumb, the item is nested into a folder and we need to make sure
// the folder is expanded.
var path = libItem.split("/");
if (path.length > 0) {
path.pop();
var folderItem = path.join("/");
lib.expandFolder(true, true, folderItem);
}
// Select the item.
lib.selectItem(libItem);
If anyone knows of a way to make sure the Library panel opens up, please leave a comment!


