Happy Apple Tablet day, everyone..

(ht)

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.

Picture 4

Here’s an example of a list of compiled classes produced when both “Generate size report” and “Export SWC” are checked:

Picture 2

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.

We’re working on a site right now whose individual pages are comprised of a stack of separately embedded swfs. Long story short, most of the content loaded into these swfs is dependent on particular flashVar data. Since I have one collection of variables intended to be global (passed into every embedded swf), I’d created a global generic object to store those name-value pairs and then assigned individualized objects per swf to the global object (var localObject = globalObject; ) before assigning that swf its unique properties. What I didn’t realize was that the JavaScript, of course, wasn’t copying the globalObject but simply referencing it, so by the time the entire page was computed there was still only one object, it just contained all properties that were suppose to instead be set per swf embed.

What this led me to illustrate through firebug introspection and flash tracing once the swfs were loaded was that even if a generic object was set with properties to be passed into a swfobject embed as flashVars, and then passed into that swfobject embed, any changes to that generic object after the embed will be reflected by the swf once loaded. So if I type in Javascript:

var flashVarsObject = {};
flashVarsObject.foo = "bar";
swfobject.embedSWF("example.swf", "example", "400", "300", "9.0.0", "expressInstall.swf", flashVarsObject);

… the flash trace for stage.loaderInfo.foo would be “bar”.
But if, after the swfobject.embedSWF call, I further set the foo variable, such as:

var flashVarsObject = {};
flashVarsObject.foo = "bar";
swfobject.embedSWF("example.swf", "example", "400", "300", "9.0.0", "expressInstall.swf", flashVarsObject);
flashVarsObject.foo = "fighters";

… the flash trace would be “fighters”.

FlashVar name/value pairs are apparently not passed in upon calling embedSWF, but rather, the flashVar object appears to simply be referenced. This of course makes just as much sense, but what seemed to make it out of place was that using firebug, I could see the actual <object> and <param> tags rendered by swfobject. At the core, thats really all swfobject does. What is strange about it is that a <param> tag includes attributes for “name” and “value”, appearing as literal strings when rendered. Somehow, this param tag synchronously represents the flashVars object passed into embedSWF, even though the param appears to be simple strings. Keep an eye out for this little catch, and of course its best to keep JavaScript objects well separated for this sort of use.

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!

Topics

Archives