Figured I may as well take care of that other caveat from the original post. I need to publicly state that I simply translated this code from Grant Skinner’s ActionScript 2 ColorMatrix class. It is with Grant’s kind permission that I can post this for anyone else to use.

This took a while to complete, and I apologize for being MIA recently. The work of this script didn’t take very long at all, but gathering my wits to blog about it just kept taking a backseat. We’ve been rather busy, and hopefully we’ll have some cool new things to share soon.

At any rate, a complete code listing is available here, and below:



function ColorMatrix(m) {
	
	this._matrix = m?m.concat():ColorMatrix.IDENTITY_MATRIX.concat();
	
	/**
	 * Copies the supplied matrix to the internal matrix.
	 * @param	m	Array
	**/
	this.copyMatrix = function(m) {
		this._matrix = m.concat();
	}
	/**
	 * Ensures that the supplied matrix Array is of the correct length.  It slices off extraneous elements, or 
	 * fills in missing elements with IDENTITY elements.
	 * @param	m	Array
	 * @return	Fixed Array
	**/
	this.fixMatrix = function(m) {
		if (m.length < ColorMatrix.LENGTH) {
			m = m.slice(0, m.length).concat(ColorMatrix.IDENTITY_MATRIX.slice(m.length,ColorMatrix.LENGTH));
		} else if (m.length > ColorMatrix.LENGTH) {
			m = m.slice(0, ColorMatrix.LENGTH);
		}
		return m;
	}
	
	/**
	 * Makes sure the incoming value in between a negative and positive limit value.
	 * @param	val		The value to clean
	 * @param	limit	The absolute value of the range.  If the limit is 100, then the value is ensured to be between -100 and 100.
	 * @return	Number; the cleaned value.
	**/
	this.cleanValue = function(val, limit) {
		return Math.min(limit, Math.max(-limit, val));
	}
	
	
	/**
	 * Core function for translating color adjustments to matrix arrays.  The internal matrix is affected.
	 * @param	m	Array
	**/
	this.multiplyMatrix = function(m) {
		var col = [];
		
		for (var i=0; i<5; i++) {
			for (j=0; j<5; j++) {
				col[j] = this._matrix[j+i*5];
			}
			for (var j=0; j<5; j++) {
				var val=0;
				for (var k=0; k<5; k++) {
					val += m[j+k*5]*col[k];
				}
				this._matrix[j+i*5] = val;
			}
		}
	}
	
	
	/**
	 * Adjusts the internal matrix array to reflect a brightness adjustment.
	 * @param	val		The brightness, from -100 to 100.
	**/
	this.adjustBrightness = function(val) {
		val = this.cleanValue(val,100);
		if (val == 0 || isNaN(val)) { return; }
		this.multiplyMatrix([
			1,0,0,0,val,
			0,1,0,0,val,
			0,0,1,0,val,
			0,0,0,1,0,
			0,0,0,0,1
		]);
	}
	
	/**
	 * @param	val	The Contrast, from -100 to 100
	**/
	this.adjustContrast = function(val) {
		val = this.cleanValue(val,100);
		if (val == 0 || isNaN(val)) { return; }
		var x;
		if (val < 0) {
			x = 127 + (val / 100) * 127
		} else {
			x = val % 1;
			if (x == 0) {
				x = ColorMatrix.DELTA_INDEX[val];
			} else {
				//x = ColorMatrix.DELTA_INDEX[(val<<0)]; // this is how the IDE does it.
				x = ColorMatrix.DELTA_INDEX[(val<<0)]*(1-x)+ColorMatrix.DELTA_INDEX[(val<<0)+1]*x; // use linear interpolation for more granularity.
			}
			x = x*127+127;
		}
		this.multiplyMatrix([
			x/127,0,0,0,0.5*(127-x),
			0,x/127,0,0,0.5*(127-x),
			0,0,x/127,0,0.5*(127-x),
			0,0,0,1,0,
			0,0,0,0,1
		]);
	}
	
	/**
	 * @param	val	The Saturation, from -100 to 100
	**/
	this.adjustSaturation = function(val) {
		val = this.cleanValue(val,100);
		if (val == 0 || isNaN(val)) { return; }
		var x    = 1+((val > 0) ? 3*val/100 : val/100);
		var lumR = 0.3086;
		var lumG = 0.6094;
		var lumB = 0.0820;
		this.multiplyMatrix([
			lumR*(1-x)+x,lumG*(1-x),lumB*(1-x),0,0,
			lumR*(1-x),lumG*(1-x)+x,lumB*(1-x),0,0,
			lumR*(1-x),lumG*(1-x),lumB*(1-x)+x,0,0,
			0,0,0,1,0,
			0,0,0,0,1
		]);
	}
	
	/**
	 * @param	val	The Hue, from -180 to 180
	**/
	this.adjustHue = function(val) {
		val = this.cleanValue(val,180)/180*Math.PI;
		if (val == 0 || isNaN(val)) { return; }
		var cosVal = Math.cos(val);
		var sinVal = Math.sin(val);
		var lumR = 0.213;
		var lumG = 0.715;
		var lumB = 0.072;
		this.multiplyMatrix([
			lumR+cosVal*(1-lumR)+sinVal*(-lumR),lumG+cosVal*(-lumG)+sinVal*(-lumG),lumB+cosVal*(-lumB)+sinVal*(1-lumB),0,0,
			lumR+cosVal*(-lumR)+sinVal*(0.143),lumG+cosVal*(1-lumG)+sinVal*(0.140),lumB+cosVal*(-lumB)+sinVal*(-0.283),0,0,
			lumR+cosVal*(-lumR)+sinVal*(-(1-lumR)),lumG+cosVal*(-lumG)+sinVal*(lumG),lumB+cosVal*(1-lumB)+sinVal*(lumB),0,0,
			0,0,0,1,0,
			0,0,0,0,1
		]);
	}
	
	
	/**
	 * Adjust all four properties at once
	 * @param	brightness (-100 - 100)
	 * @param	contrast (-100 - 100)
	 * @param	saturation (-100 - 100)
	 * @param	hue (-180 - 180)
	**/
	this.adjustColor = function(brightness, contrast, saturation, hue) {
		this.adjustHue(hue);
		this.adjustContrast(contrast);
		this.adjustBrightness(brightness);
		this.adjustSaturation(saturation);
	}
	
	
	
	
	
	this.__defineGetter__("matrix", function() {return this._matrix.concat(); });
	this.toString = function() {
		//return "ColorMatrix: " + this._matrix.toString();
		var out = "ColorMatrix:\n";
		var v;
		var iLen = this._matrix.length;
		var l = 4;
		for (var i = 0; i < iLen; i++) {
			v = this._matrix[i].toString();
			if (v.length > l) {
				v = v.substr(0, l);
			} else {
				while (v.length < l) {
					v += " ";
				}
			}
			out += v;
			if (i % 5 == 4) {
				out += "\n";
			} else {
				out += " ";
			}
		}
		return out;
	}
	
}

ColorMatrix._DELTA_INDEX = [
	0,    0.01, 0.02, 0.04, 0.05, 0.06, 0.07, 0.08, 0.1,  0.11,
	0.12, 0.14, 0.15, 0.16, 0.17, 0.18, 0.20, 0.21, 0.22, 0.24,
	0.25, 0.27, 0.28, 0.30, 0.32, 0.34, 0.36, 0.38, 0.40, 0.42,
	0.44, 0.46, 0.48, 0.5,  0.53, 0.56, 0.59, 0.62, 0.65, 0.68,
	0.71, 0.74, 0.77, 0.80, 0.83, 0.86, 0.89, 0.92, 0.95, 0.98,
	1.0,  1.06, 1.12, 1.18, 1.24, 1.30, 1.36, 1.42, 1.48, 1.54,
	1.60, 1.66, 1.72, 1.78, 1.84, 1.90, 1.96, 2.0,  2.12, 2.25,
	2.37, 2.50, 2.62, 2.75, 2.87, 3.0,  3.2,  3.4,  3.6,  3.8,
	4.0,  4.3,  4.7,  4.9,  5.0,  5.5,  6.0,  6.5,  6.8,  7.0,
	7.3,  7.5,  7.8,  8.0,  8.4,  8.7,  9.0,  9.4,  9.6,  9.8,
	10.0
];
ColorMatrix._IDENTITY_MATRIX = [
	1,0,0,0,0,
	0,1,0,0,0,
	0,0,1,0,0,
	0,0,0,1,0,
	0,0,0,0,1
]
ColorMatrix._LENGTH = ColorMatrix._IDENTITY_MATRIX.length;


ColorMatrix.__defineGetter__("DELTA_INDEX",     function() { return ColorMatrix._DELTA_INDEX; } );
ColorMatrix.__defineGetter__("IDENTITY_MATRIX", function() { return ColorMatrix._IDENTITY_MATRIX; } );
ColorMatrix.__defineGetter__("LENGTH",          function() { return ColorMatrix._LENGTH; } );







var sel = fl.getDocumentDOM().selection;
fl.outputPanel.clear();


var iLen = sel.length;
var qualities = {
	low:1,
	medium:2,
	high:3
}
for (var i = 0; i < iLen; i++) {
	var s = sel[i];
	if (s.instanceType != "symbol") continue;
	// fl.trace("FILTERS FOR " + (s.name == "" ? "UNNAMED INSTANCE" : s.name) + " -------------------------------------");
	var filters = s.filters;
	var jLen = filters.length;
	for (var j = 0; j < jLen; j++) {
		var f = filters[j];
		
		// for (var p in f) {
		// 	fl.trace(p + " :: " + f[p]);
		// }
		
		var en = f.enabled ? "" : "//";
		var q = qualities[f.quality];
		if (f.color) {
			var colorInfo = getColorAndAlpha(f.color);
		}
		if (f.highlightColor) {
			var highlightColorInfo = getColorAndAlpha(f.highlightColor);
		}
		if (f.shadowColor) {
			var shadowColorInfo = getColorAndAlpha(f.shadowColor);
		}
		if (f.colorArray) {
			var colorArray = [];
			var alphaArray = [];
			var iLen = f.colorArray.length;
			for (var i = 0; i < iLen; i++) {
				var info = getColorAndAlpha(f.colorArray[i]);
				colorArray.push(info.color);
				alphaArray.push (info.alpha);
			}
		}
		var s = f.strength / 100;
		
		switch (f.name) {
			case "glowFilter":
				fl.trace(en + "new GlowFilter("+colorInfo.color+", "+colorInfo.alpha+", "+f.blurX+", "+f.blurY+", "+s+", "+q+", "+f.inner+", "+f.knockout+");");
				break;
			case "dropShadowFilter":
				fl.trace(en + "new DropShadowFilter("+f.distance+", "+f.angle+", "+colorInfo.color+", "+colorInfo.alpha+", "+f.blurX+", "+f.blurY+", "+s+", "+q+", "+f.inner+", "+f.knockout+", "+f.hideObject+");");
				break;
			case "blurFilter":
				fl.trace(en + "new BlurFilter("+f.blurX+", "+f.blurY+", "+q+");");
				break;
			case "bevelFilter":
				fl.trace(en + "new BevelFilter("+f.distance+", "+f.angle+", "+highlightColorInfo.color+", "+highlightColorInfo.alpha+", "+shadowColorInfo.color+", "+shadowColorInfo.alpha+", "+f.blurX+", "+f.blurY+", "+f.strength+", "+q+", \""+f.type+"\", "+f.knockout+");");
				break;
			case "gradientBevelFilter":
				fl.trace(en + "new GradientBevelFilter("+f.distance+", "+f.angle+", ["+colorArray+"], ["+alphaArray+"], ["+f.posArray+"], "+f.blurX+", "+f.blurY+", "+f.strength+", "+q+", \""+f.type+"\", "+f.knockout+");");
				break;
			case "gradientGlowFilter":
				fl.trace(en + "new GradientGlowFilter("+f.distance+", "+f.angle+", ["+colorArray+"], ["+alphaArray+"], ["+f.posArray+"], "+f.blurX+", "+f.blurY+", "+f.strength+", "+q+", \""+f.type+"\", "+f.knockout+");");
				break;
			case "adjustColorFilter":
				var m = new ColorMatrix();
				m.adjustColor(f.brightness, f.contrast, f.saturation, f.hue);
				fl.trace(en + "new ColorMatrixFilter(["+m.matrix+"]);");
				break;
		}
		
	}
	fl.trace("\n");
}

/**
 * Returns an Object with two properties, color and alpha, as strings, based on the parsing of the color string coming in.  The color string
 * will be something like "#FF9933" or "#FF993366".  If it's a 32-bit color, the alpha channel is contained in the last two bytes 
 * (66 in this case).
**/
function getColorAndAlpha(color) {
	var colorInfo = {};
	if (color.length == 9) {
		colorInfo.alpha = Math.round(parseInt(color.substr(7, 2), 16) / 0xFF * 100) / 100;
		colorInfo.color = color.substr(0, 7).replace(/^#/, "0x")
	} else if (color.length == 7) {
		colorInfo.alpha = 1;
		colorInfo.color = color.replace(/^#/, "0x")
	} else {
		fl.trace("Problem parsing color.")
	}
	return colorInfo;
}


Thank you, Grant, for doing all of the hard work on this one!

This is a follow up to yesterday’s post, addressing the first of the two caveats I mentioned then. I had said that the alpha associated with a color (whether it’s the color of the glow or shadow, or highlight of a bevel, or color stop in a gradient) was nowhere to be found in JSFL’s filter object, and that I had just used a value of 1.0 in the generated code.

Turns out I was wrong, but I’m glad that we do access to this information.

Honestly, I can’t believe I didn’t notice it before when I was writing the original script. If a given color has an alpha of anything other than 100%, the color string is 32-bit, like “#FF993366“. I think what threw me off was that the alpha channel is in the right-most bits, not the left-most bits, like we’re used to with specifying color-with-alpha in ActionScript, like when working with BitmapData. It’s a lame excuse, but I think I was scanning the color values of my test filters, looking for “#66FF9933“, but instead seeing the initial “FF” and dismissing the value as being 24-bit.

Anyway, there’s no handy way to extract the alpha information from the color information, but since it’s actually a string of a hex-format numbers, it’s fairly simple to do some string manipulation and get the values. I’ve updated the original JSFL script with a bit of logic to handle this extraction, as well as updating the use of the colors from the Filter object when converting to ActionScript code. The key color extraction function is this:

function getColorAndAlpha(color) {
	var colorInfo = {};
	if (color.length == 9) {
		colorInfo.alpha = Math.round(parseInt(color.substr(7, 2), 16) / 0xFF * 100) / 100;
		colorInfo.color = color.substr(0, 7).replace(/^#/, "0x")
	} else if (color.length == 7) {
		colorInfo.alpha = 1;
		colorInfo.color = color.replace(/^#/, "0x")
	} else {
		fl.trace("Problem parsing color.")
	}
	return colorInfo;
}

Pass in the color string from a Filter object, and get back an object with color and alpha properties back, both ready for use in the ActionScript conversion.

The updated script can be found on the original post, or here is the link to the text file again.

The title kinda says it all; if you were creating filters visually (as I was the other day) but needed to ultimately produce them dynamically in ActionScript (as I did), then you may wish (as I did) for a JSFL command to spit out the necessary ActionScript based on your visual, on-stage filters.

As you can guess, I went ahead and wrote that JSFL command. Here it is (viewable as a text file here):

var sel = fl.getDocumentDOM().selection;
fl.outputPanel.clear();

var iLen = sel.length;
var qualities = {
	low:1,
	medium:2,
	high:3
}
for (var i = 0; i < iLen; i++) {
	var s = sel[i];
	if (s.instanceType != "symbol") continue;
	fl.trace("FILTERS FOR " + (s.name == "" ? "UNNAMED INSTANCE" : s.name) + " -------------------------------------");
	var filters = s.filters;
	var jLen = filters.length;
	for (var j = 0; j < jLen; j++) {
		var f = filters[j];

		for (var p in f) {
			fl.trace(p + " :: " + f[p]);
		}

		var en = f.enabled ? "" : "//";
		var q = qualities[f.quality];
		if (f.color) {
			var colorInfo = getColorAndAlpha(f.color);
		}
		if (f.highlightColor) {
			var highlightColorInfo = getColorAndAlpha(f.highlightColor);
		}
		if (f.shadowColor) {
			var shadowColorInfo = getColorAndAlpha(f.shadowColor);
		}
		if (f.colorArray) {
			var colorArray = [];
			var alphaArray = [];
			var iLen = f.colorArray.length;
			for (var i = 0; i < iLen; i++) {
				var info = getColorAndAlpha(f.colorArray[i]);
				colorArray.push(info.color);
				alphaArray.push (info.alpha);
			}
		}
		var s = f.strength / 100;

		switch (f.name) {
			case "glowFilter":
				fl.trace(en + "new GlowFilter("+colorInfo.color+", "+colorInfo.alpha+", "+f.blurX+", "+f.blurY+", "+s+", "+q+", "+f.inner+", "+f.knockout+")");
				break;
			case "dropShadowFilter":
				fl.trace(en + "new DropShadowFilter("+f.distance+", "+f.angle+", "+colorInfo.color+", "+colorInfo.alpha+", "+f.blurX+", "+f.blurY+", "+s+", "+q+", "+f.inner+", "+f.knockout+", "+f.hideObject+")");
				break;
			case "blurFilter":
				fl.trace(en + "new BlurFilter("+f.blurX+", "+f.blurY+", "+q+")");
				break;
			case "bevelFilter":
				fl.trace(en + "new BevelFilter("+f.distance+", "+f.angle+", "+highlightColorInfo.color+", "+highlightColorInfo.alpha+", "+shadowColorInfo.color+", "+shadowColorInfo.alpha+", "+f.blurX+", "+f.blurY+", "+f.strength+", "+q+", \""+f.type+"\", "+f.knockout+")");
				break;
			case "gradientBevelFilter":
				fl.trace(en + "new GradientBevelFilter("+f.distance+", "+f.angle+", ["+colorArray+"], ["+alphaArray+"], ["+f.posArray+"], "+f.blurX+", "+f.blurY+", "+f.strength+", "+q+", \""+f.type+"\", "+f.knockout+")");
				break;
			case "gradientGlowFilter":
				fl.trace(en + "new GradientGlowFilter("+f.distance+", "+f.angle+", ["+colorArray+"], ["+alphaArray+"], ["+f.posArray+"], "+f.blurX+", "+f.blurY+", "+f.strength+", "+q+", \""+f.type+"\", "+f.knockout+"");
				break;
			case "adjustColorFilter":
				var m = new ColorMatrix();
				m.adjustColor(f.brightness, f.contrast, f.saturation, f.hue);
				fl.trace(en + "new ColorMatrixFilter(["+m.matrix+"]);");
				break;
		}

	}
	fl.trace("\n");
}

/**
 * Returns an Object with two properties, color and alpha, as strings, based on the parsing of the color string coming in.  The color string
 * will be something like "#FF9933" or "#FF993366".  If it's a 32-bit color, the alpha channel is contained in the last two bytes
 * (66 in this case).
**/
function getColorAndAlpha(color) {
	var colorInfo = {};
	if (color.length == 9) {
		colorInfo.alpha = Math.round(parseInt(color.substr(7, 2), 16) / 0xFF * 100) / 100;
		colorInfo.color = color.substr(0, 7).replace(/^#/, "0x")
	} else if (color.length == 7) {
		colorInfo.alpha = 1;
		colorInfo.color = color.replace(/^#/, "0x")
	} else {
		fl.trace("Problem parsing color.")
	}
	return colorInfo;
}

// CLASSES ============================================================================

/**
 * Allows for conversion of simple color adjustment properties into an ActionScript-ready matrix Array.
**/
function ColorMatrix(m) {

	this._matrix = m?m.concat():ColorMatrix.IDENTITY_MATRIX.concat();

	/**
	 * Copies the supplied matrix to the internal matrix.
	 * @param	m	Array
	**/
	this.copyMatrix = function(m) {
		this._matrix = m.concat();
	}
	/**
	 * Ensures that the supplied matrix Array is of the correct length.  It slices off extraneous elements, or
	 * fills in missing elements with IDENTITY elements.
	 * @param	m	Array
	 * @return	Fixed Array
	**/
	this.fixMatrix = function(m) {
		if (m.length  ColorMatrix.LENGTH) {
			m = m.slice(0, ColorMatrix.LENGTH);
		}
		return m;
	}

	/**
	 * Makes sure the incoming value in between a negative and positive limit value.
	 * @param	val		The value to clean
	 * @param	limit	The absolute value of the range.  If the limit is 100, then the value is ensured to be between -100 and 100.
	 * @return	Number; the cleaned value.
	**/
	this.cleanValue = function(val, limit) {
		return Math.min(limit, Math.max(-limit, val));
	}

	/**
	 * Core function for translating color adjustments to matrix arrays.  The internal matrix is affected.
	 * @param	m	Array
	**/
	this.multiplyMatrix = function(m) {
		var col = [];

		for (var i=0; i<5; i++) {
			for (j=0; j<5; j++) {
				col[j] = this._matrix[j+i*5];
			}
			for (var j=0; j<5; j++) {
				var val=0;
				for (var k=0; k<5; k++) {
					val += m[j+k*5]*col[k];
				}
				this._matrix[j+i*5] = val;
			}
		}
	}

	/**
	 * Adjusts the internal matrix array to reflect a brightness adjustment.
	 * @param	val		The brightness, from -100 to 100.
	**/
	this.adjustBrightness = function(val) {
		val = this.cleanValue(val,100);
		if (val == 0 || isNaN(val)) { return; }
		this.multiplyMatrix([
			1,0,0,0,val,
			0,1,0,0,val,
			0,0,1,0,val,
			0,0,0,1,0,
			0,0,0,0,1
		]);
	}

	/**
	 * @param	val	The Contrast, from -100 to 100
	**/
	this.adjustContrast = function(val) {
		val = this.cleanValue(val,100);
		if (val == 0 || isNaN(val)) { return; }
		var x;
		if (val < 0) {
			x = 127 + (val / 100) * 127
		} else {
			x = val % 1;
			if (x == 0) {
				x = ColorMatrix.DELTA_INDEX[val];
			} else {
				//x = ColorMatrix.DELTA_INDEX[(val<<0)]; // this is how the IDE does it.
				x = ColorMatrix.DELTA_INDEX[(val<<0)]*(1-x)+ColorMatrix.DELTA_INDEX[(val< 0) ? 3*val/100 : val/100);
		var lumR = 0.3086;
		var lumG = 0.6094;
		var lumB = 0.0820;
		this.multiplyMatrix([
			lumR*(1-x)+x,lumG*(1-x),lumB*(1-x),0,0,
			lumR*(1-x),lumG*(1-x)+x,lumB*(1-x),0,0,
			lumR*(1-x),lumG*(1-x),lumB*(1-x)+x,0,0,
			0,0,0,1,0,
			0,0,0,0,1
		]);
	}

	/**
	 * @param	val	The Hue, from -180 to 180
	**/
	this.adjustHue = function(val) {
		val = this.cleanValue(val,180)/180*Math.PI;
		if (val == 0 || isNaN(val)) { return; }
		var cosVal = Math.cos(val);
		var sinVal = Math.sin(val);
		var lumR = 0.213;
		var lumG = 0.715;
		var lumB = 0.072;
		this.multiplyMatrix([
			lumR+cosVal*(1-lumR)+sinVal*(-lumR),lumG+cosVal*(-lumG)+sinVal*(-lumG),lumB+cosVal*(-lumB)+sinVal*(1-lumB),0,0,
			lumR+cosVal*(-lumR)+sinVal*(0.143),lumG+cosVal*(1-lumG)+sinVal*(0.140),lumB+cosVal*(-lumB)+sinVal*(-0.283),0,0,
			lumR+cosVal*(-lumR)+sinVal*(-(1-lumR)),lumG+cosVal*(-lumG)+sinVal*(lumG),lumB+cosVal*(1-lumB)+sinVal*(lumB),0,0,
			0,0,0,1,0,
			0,0,0,0,1
		]);
	}

	/**
	 * Adjust all four properties at once
	 * @param	brightness (-100 - 100)
	 * @param	contrast (-100 - 100)
	 * @param	saturation (-100 - 100)
	 * @param	hue (-180 - 180)
	**/
	this.adjustColor = function(brightness, contrast, saturation, hue) {
		this.adjustHue(hue);
		this.adjustContrast(contrast);
		this.adjustBrightness(brightness);
		this.adjustSaturation(saturation);
	}

	this.__defineGetter__("matrix", function() {return this._matrix.concat(); });
	this.toString = function() {
		//return "ColorMatrix: " + this._matrix.toString();
		var out = "ColorMatrix:\n";
		var v;
		var iLen = this._matrix.length;
		var l = 4;
		for (var i = 0; i  l) {
				v = v.substr(0, l);
			} else {
				while (v.length < l) {
					v += " ";
				}
			}
			out += v;
			if (i % 5 == 4) {
				out += "\n";
			} else {
				out += " ";
			}
		}
		return out;
	}

}

ColorMatrix._DELTA_INDEX = [
	0,    0.01, 0.02, 0.04, 0.05, 0.06, 0.07, 0.08, 0.1,  0.11,
	0.12, 0.14, 0.15, 0.16, 0.17, 0.18, 0.20, 0.21, 0.22, 0.24,
	0.25, 0.27, 0.28, 0.30, 0.32, 0.34, 0.36, 0.38, 0.40, 0.42,
	0.44, 0.46, 0.48, 0.5,  0.53, 0.56, 0.59, 0.62, 0.65, 0.68,
	0.71, 0.74, 0.77, 0.80, 0.83, 0.86, 0.89, 0.92, 0.95, 0.98,
	1.0,  1.06, 1.12, 1.18, 1.24, 1.30, 1.36, 1.42, 1.48, 1.54,
	1.60, 1.66, 1.72, 1.78, 1.84, 1.90, 1.96, 2.0,  2.12, 2.25,
	2.37, 2.50, 2.62, 2.75, 2.87, 3.0,  3.2,  3.4,  3.6,  3.8,
	4.0,  4.3,  4.7,  4.9,  5.0,  5.5,  6.0,  6.5,  6.8,  7.0,
	7.3,  7.5,  7.8,  8.0,  8.4,  8.7,  9.0,  9.4,  9.6,  9.8,
	10.0
];
ColorMatrix._IDENTITY_MATRIX = [
	1,0,0,0,0,
	0,1,0,0,0,
	0,0,1,0,0,
	0,0,0,1,0,
	0,0,0,0,1
]
ColorMatrix._LENGTH = ColorMatrix._IDENTITY_MATRIX.length;

ColorMatrix.__defineGetter__("DELTA_INDEX",     function() { return ColorMatrix._DELTA_INDEX; } );
ColorMatrix.__defineGetter__("IDENTITY_MATRIX", function() { return ColorMatrix._IDENTITY_MATRIX; } );
ColorMatrix.__defineGetter__("LENGTH",          function() { return ColorMatrix._LENGTH; } );

I have tested this with a variety of symbol instances with a variety of filters applied to them. Seems to work for me, but I really only used it for my recent application. Please let us know if you run into issues that break the script or produce erroneous results.

A few caveats:

This script doesn’t do two things, both of which are significant to know about.

(UPDATE: I have since resolved the first item below. See this post for more information. The code above as well as the linked text file have been updated with the new code)

(UPDATE 2: I’ve also resolved the second item. This post has more information, and the code on this page has been updated.)

  1. For some reason, the JSFL Filter object doesn’t contain information about the color’s alpha of a filter. All filters except for the Blur and Adjust Color (ColorMatrixFilter) have an option for alpha, both in the IDE’s interface and in the ActionScript object. But this information is not contained in the JSFL information. Go figure. So, in all cases where an alpha parameter is expected in the ActionScript constructor, I supply a value of 1.
  2. I trust it’s possible to work out the logic to convert the Adjust Color filter in the IDE to a ColorMatrixFilter in ActionScript, but that requires conversion of “saturation” and “contrast” settings in the IDE to an Array of numbers in ActionScript. I hope to eventually do this, but for now, I just wanted to post this script.

I also hope to reconsider how to abstract some of this functionality into some JSFL classes. Someday…

So, feel free to use, and I hope I just saved you a few minutes.

Summit had a few entries at the Rosey Awards this year. We came away three Excellence Awards (which, as our Creative Director Eddy put it, is one among a few of the best, like being nominated but not winning an Oscar). These were for the Nike Golf website, the Nike Golf Outerwear microsite, and for our own Summit Projects website.

Better yet, we came home with a “Best of Show” award, in the motion graphics category, for the Nike Golf SQ Machspeed Driver video (sadly, not actually online anymore, although you can see it on the Roseys’ site here).

Here are some photos from the event, which had an open bar and (accordingly) a lot of good times (photos taken by Eddy).

The main event room


Here's the award for Motion Design, on display in the Award Room


We did get to pick up the award and bring it home. The computer playing the video had to stay.

It’s an honor to be a part of the team that does this amazing work! Working on Nike Golf alone is a constant challenge and surprise. Our creative team really pitches some awesome stuff, and then we technical folk get to build it. It’s a good day to be a Summiteer.

The Problem

I don’t know about everyone else, but this has happened to me more times than I can count. For one reason or another, the link between document class and FLA gets lost. Maybe you moved the FLA file and now it can’t find the class path, or maybe you refactored your code and put the class in a new package, and forgot to update the FLA file.

Of course, when you ask Flash to verify your document class, it will tell you if it can’t find one. But that’s only if you ask. If you open up a file that had worked in the past, but now doesn’t, Flash won’t tell you. It will simply create the supposed class for you, as an empty MovieClip subclass, when you publish the file.

The solution

For some reason, this has happened to me enough that I gave it some thought, and have come up with a rudimentary way of asking Flash to check the validity of the document class for you. This involves a JSFL script that checks for the existence of a file, derived from the document class name and the various class paths available to that document. Most importantly, it registers an event listener for documentChanged, so this check will run any time you open up or switch to a different a Flash file.

The issue I’m facing right now is making it happen automatically; right now you have to execute the script, at which point the listener gets set up and you’re good to go, but you have to still execute the script. It would be nice to have it execute right away once Flash starts up. Seems like a WindowSWF or a Tool could accomplish that, but both of those would require that you have extension installed and open as part of your workspace.

The Important Things to Note

Please note these two important things:

  1. The script is Mac-only as written. This is easily fixed, all you have to do is update the tmpFilePath variable as appropriate.
  2. The script depends on the SourcePath class. You can find out more about that in this post, and you may wish/need to update the first line of the script so that it loads the SourcePath class from the place where you’ve installed it. Oh, to have a more robust class management system with JSFL.

The Script

I banged this out pretty quick, because I’m supposed to be doing other things, so, there’s room for improvement. Here’s the source code (viewable and downloadable at this link, as well):

fl.runScript(fl.configURI + "Commands/lib/SourcePath.jsfl");

var tmpFilePath = "file:///tmp/docClassCheckerIds.txt";

if (FLfile.exists(tmpFilePath)) {
	var contents = FLfile.read(tmpFilePath);
	var lines = contents.split("\n");
	var iLen = lines.length;
	for (var i = 0; i < iLen; i++) {
		var line = lines[i];
		var pieces = line.split("=");
		var eventType = pieces[0];
		var eventId   = pieces[1];
		fl.removeEventListener(eventType, parseInt(eventId));
	}
}

var changeId = fl.addEventListener("documentChanged", onDocumentOpened);

FLfile.write(tmpFilePath, "documentChanged=" + changeId);

function onDocumentOpened() {
	fl.trace("Event happened.")
	testForDocumentClass();
}


function testForDocumentClass() {
	
	var doc = fl.getDocumentDOM();
	if (!doc) return;
	if (doc.asVersion < 3) return;
	
	var documentClass = doc.docClass;
	if (documentClass == "") return;

	var source = new SourcePath();
	var classPaths = source.pathsURI;

	var iLen = classPaths.length;
	var classPath;
	var docClassPath = documentClass.replace(/\./g, "/") + ".as";
	for (var i = 0; i < iLen; i++) {
		classPath = classPaths[i];
		if (FLfile.exists(classPath + docClassPath)) {
			// We found a matching file, so no further action is required.
			return;
		}
	}
	
	// If we got here, we didn't break the loop on success, so we didn't find a Document Class.
	alert("Couldn't find a document class called \""+documentClass+"\" for this document.");
}


testForDocumentClass();

The Geeky Discussion

If you’re interested in some of the details, the logic is pretty straightforward, thanks to the SourcePath class. The bulk of it involves just looping over all of the available source paths, concatenating that to a file-system version of the stated document class for the file, and checking to see if a file exists at that path. If we get just one match, then we’re good. If we have no matches, we throw up an alert box.

I spent a little extra time on the event management. Because I was afraid of accidentally running the script more than once in a session, I wanted to make sure we removed any previously existing event listeners. This was troublesome, however, thanks to the undocumented changes in how the event listener functions operate. See this post for more details.

My solution involved capturing the event IDs, and writing them to a temporary file. Then, if we can get the ids out of this file, we remove the event listener based on this id before we add it. That is, if the script is run a second time, we’ll clean up the events set in the first run. Kinda laborious, but at least it works. Otherwise, you run the risk of seeing that alert several times in a row.

Based on previous ramblings on “dynamic XUL,” and seeing as I needed that technique again recently, I decided it might be worthwhile to wrap it up in a class. I got a little over-ambitious on this one, and decided to try and do away with the yucky write-XML-in-JavaScript-as-a-String business. So the new class not only handles the creation, execution, and cleanup of an on-the-fly XUL window, but you can optionally also use instance methods to create your XUL controls without writing XML directly. Sound neat? WELL, IT IS!

First up, some example code.

Simplifying the XUL File Process

In this example, I’m taking the example from the post of dynamic XUL and using the XULWindow class to abstract away the process of creating, using, and deleting the temporary XUL file.

fl.runScript(fl.configURI + "Commands/lib/SourcePath.jsfl");
fl.runScript(fl.configURI + "Commands/lib/XULWindow.jsfl");

var sources = new SourcePath();
var window = new XULWindow();

var classPathXul = "";
for (var i = 0; i < sources.paths.length; i++) {
	classPathXul += '		<radio id="'+(i.toString())+'" label="'+sources.paths[i]+'" />\n'
}

var xul = '<dialog title="Select a class path in which to create classes" buttons="accept,cancel">\n'
+ '	<radiogroup id="selection">\n'
+ 		classPathXul
+ '	</radiogroup>\n'
+ '</dialog>';

var classPath = window.create(xul);

if (classPath.dismiss == "accept") {
	fl.trace("classPath.selection: " + classPath.selection);
} else {
	// Bye.
}

Notice that the lines involving creating and writing the temporary XUL file are gone, instead, we create a new XUL window object and tell it to create, passing in the XUL string we’ve generated.

I’ve also changed the class to use the SourcePath class, further simplifying the script.

Simplifying the XUL Creation Process

Here, I’ve rewritten the example yet again, this time I’m taking advantage of the methods provided by XULWindow to avoid having to write the XUL myself.

fl.runScript(fl.configURI + "Commands/lib/SourcePath.jsfl");
fl.runScript(fl.configURI + "Commands/lib/XULWindow.jsfl");

var sources = new SourcePath();
var window = new XULWindow("Select a class path in which to create classes", XULWindow.ACCEPT, XULWindow.CANCEL);

var classPaths = [];
for (var i = 0; i < sources.paths.length; i++) {
	classPaths.push({label:sources.paths[i]})
}
window.addRadioGroup("selection", classPaths)

var classPath = window.create();

if (classPath.dismiss == "accept") {
	fl.trace("classPath.selection: " + classPath.selection);
} else {
	// Bye.
}

Now, we’re passing in key parameters to the XULWindow constructor, and using addRadioGroup to create the controls on the window. A call to create, with no XUL string passed in, will use the internally constructor XUL, based on the other input received to the object. The end result is the same.

Documentation

I worked up some quick ASDoc for the class, which can be found here (or the combined documentation is now available here).

Source Code

Finally, here is the source code of the class itself, which is also posted here, as well as documentation here.

/**
 * Represents a XUL panel in JSFL.  The object can be set up and then used to invoke a XUL panel without writing an actual XUL file.
 * The XML can be generated purely in JSFL and passed in, or you can use convenience methods to add various controls without having
 * to writing XML strings in JSFL.
 * 
 * <p>For full-on XUL reference, the Mozilla document is here: <a href="https://developer.mozilla.org/en/XUL_Reference">
 * https://developer.mozilla.org/en/XUL_Reference</a>.  Note that Flash supports a very small subset of this.</p>
 *
**/


/** 
 * Constructor
 * @param	title	The title of the window.  This is only used if you own XML string is not passed in to the <code>create</code>
 * 					method.
 * @param	buttons	The remaining arguments are Strings of types of buttons to include.  For convenience, use the static
 * 					constants XULWindow.ACCEPT and XULWindow.CANCEL.  This is only used if you own XML string is not passed in to 
 * 					the <code>create</code> method.
**/
 function XULWindow(title) {
	var that = this;
	this._title = title || "Options";
	var buttons = [];
	var iLen = arguments.length;
	for (var i = 1; i < iLen; i++) {
		buttons.push(arguments[i]);
	}
	this._xml = '<dialog title="'+title+'" buttons="'+buttons.join(",")+'">';
	this._tab = 1;
	
	
	/**
	 * Creates a XUL window panel.  It is attached to the current document
	 * 
	 * @param	xul		A String of raw XUL to create.  If null, the window will be created according to the various control
	 * 					creation methods.  If not null, any other method calls would be ignored and the passed-in XUL is used.
	 * @return	An object containing information about the user input.  The object will have a property called <code>dismiss</code>
	 * 			that contains the name of the button that was clicked (either <code>XULWindow.ACCEPT</code> or 
	 * 			&lt;code>XULWindow.CANCEL</code>).  It will
	 * 			also have other properties, named after the various controls' id values.  The value contained by these properties
	 * 			will be the value associated with the input of the control.
	**/
	this.create = function(xul) {
		var doc = fl.getDocumentDOM();
		if (!doc) {
			alert("There is no open document.  XUL Windows need to be attached to a document. [XULWindow::create()]");
			return;
		}
		
		var xulFilePath = fl.configURI + escape(this._title) + ".xul";
	
		if (xul==null) xul = this._xml + "</dialog>";
		
		FLfile.write(xulFilePath, xul);
		var options = fl.getDocumentDOM().xmlPanel(xulFilePath);
		FLfile.remove(xulFilePath);
		
		return options//.dismiss;
		
	}
	
	/**
	 * Creates a check box control with a label.
	 * @param	label	String for the label next to the check box.
	 * @param	id		String for the id of the checkbox.  This is the name of the property on the returned object with data in it
	 * @param	checked	Whether initially checked or not.
	**/
	this.addCheckbox = function(label, id, checked) {
		this._xml += '<checkbox label="'+label+'" id="'+id+'" checked="'+(checked?"true":"false")+'" />';
	}
	
	/**
	 * Opens an &lt;hbox&gt; nodes so that all controls added after this method call are contained within the hbox.  Should
	 * be ultimately terminated with <a href="#closeHBox">closeHBox</a>
	 * 
	 * @see #closeHBox
	**/
	this.openHBox = function() {
		this._xml += "<hbox>";
	}
	
	/**
	 * Closes a previously open &lt;hbox&gt;, enclosing all intermediate controls within an hbox node.
	 * 
	 * @see #openHBox
	**/
	this.closeHBox = function() {
		this._xml += "</hbox>";
	}
	
	/**
	 * Opens an &lt;vbox&gt; nodes so that all controls added after this method call are contained within the vbox.  Should
	 * be ultimately terminated with <a href="#closeVBox">closeVBox</a>
	 * 
	 * @see #closeVBox
	**/
	this.openVBox = function() {
		this._xml += "<vbox>";
	}
	
	/**
	 * Closes a previously open &lt;vbox&gt;, enclosing all intermediate controls within an vbox node.
	 * 
	 * @see #openVBox
	**/
	this.closeVBox = function() {
		this._xml += "</vbox>";
	}
	
	
	/**
	 * @private
	 * Adds a button to the panel, but I assume there might be more involved in getting one to do something than we can realistically
	 * accomplish here.
	**/
	this.addButton = function(label, id, accessKey, autocheck) {
		this._xml += '<button label="'+label+'" id="'+id+'" accesskey="'+accessKey+'" autocheck="'+autocheck+'" tabindex="'+(this._tab++)+'" />';
	}
	
	/**
	 * Creates a List Box.
	 * @param	id		The name of the property on the returned object with data in it.
	 * @param	items	Array of Objects specifying the items to display.  The Objects should take on the following form:
	 * 						<code>{label:"Text to display", value:"valueOfThisItem", selected:true}</code>
	 * 						<code>label</code> is required.  If <code>value</code> is omitted, the <code>label</code> is returned
	 * 						as the selected value.
	 * @param	width	The width in pixels. Defaults to 200.
	 * @param	rows	The number of rows to display in the viewable area.  If the <code>rows</code> is less than the number of
	 * 					items in the list, scrollbars will appear.  If this parameter is omitted, the length of the Array passed
	 * 					in to <code>items</code> is used.
	**/
	this.addListBox = function(id, items, width, rows) {
		var signature = "addListBox(id:String, items:Array, width:Number=NaN, rows:Number=NaN)"
		if (!id) {
			alert("XULWindow::addListBox() requires an id parameter.\n\t" + signature);
			return;
		}
		if (!items) {
			alert("XULWindow::addListBox() requires an items parameter.\n\t" + signature);
			return;
		}
		if (isNaN(width)) { width = 200; }
		if (isNaN(rows)) { rows = items.length; }
		
		this._xml += '<listbox id="'+id+'" width="'+width+'" rows="'+rows+'">\n';
		var iLen = items.length;
		var item;
		var selected;
		var value;
		for (var i = 0; i < iLen; i++) {
			item = items[i];
			if (!item.label) {
				alert("The items parameter of XULWindow::addListBox() needs to be an Array of Objects, each Object consisting of"
				+ " at least a label property, and optionally a value and a selected property.")
				return;
			}
			value    = item.value    ? 'value="'+item.value+'" ' : '';
			selected = item.selected ? 'selected="'+item.selected+'" ' : '';
			
			this._xml += '\t<listitem '+value+'label="' + item.label + '" '+selected+'/>\n';
		}
		this._xml += "</listbox>\n";
	}
	
	/**
	 * Adds a basic text label.
	 * @param	label	The text to display in the label.
	 * @param	control	The id of the associated control.  If the user clicks on the label, focus is moved to the control. (Doesn't work?)
	**/
	this.addLabel = function(label, control) {
		this._xml += '<label value="'+label+'" control="'+control+'"/>\n';
	}
	
	/**
	 * Adds a text box in which the user can type.
	 * @param	id		The name of the property on the returned data object.
	 * @param	value	The default value contained within the text box.
	**/
	this.addTextBox = function(id, value) {
		
		this._xml += '<textbox id="'+id+'" value="'+value+'" />'
	}
	
	/**
	 * Creates a vertically-oriented group of radio buttons.
	 * @param	id		The name of the property on the returned object with data in it.
	 * @param	items	Array of Objects specifying the buttons to display.  The Objects should take on the following form:
	 * 						<code>{label:"Text to display", value:"valueOfThisButton", selected:true}</code>
	 * 						<code>label</code> is required.  If <code>value</code> is omitted, the <code>label</code> is returned
	 * 						as the selected value.
	**/
	this.addRadioGroup = function(id, items) {
		var signature = "addRadioGroup(id:String, items:Array)"
		if (!id) {
			alert("XULWindow::addRadioGroup() requires an id parameter.\n\t" + signature);
			return;
		}
		if (!items) {
			alert("XULWindow::addRadioGroup() requires an items parameter.\n\t" + signature);
			return;
		}
		
		this._xml += '<radiogroup id="'+id+'">\n';
		var iLen = items.length;
		var item;
		var selected;
		var value;
		for (var i = 0; i < iLen; i++) {
			item = items[i];
			if (!item.label) {
				alert("The items parameter of XULWindow::addRadioGroup() needs to be an Array of Objects, each Object consisting of"
				+ " at least a label property, and optionally a value and a selected property.")
				return;
			}
			value    = item.value    ? 'value="'+item.value+'" ' : '';
			selected = item.selected ? 'selected="'+item.selected+'" ' : '';
			
			this._xml += '\t<radio '+value+'label="' + item.label + '" '+selected+'/>\n';
		}
		this._xml += "</radiogroup>\n";
		
	}
}


/**
 * Static constant alias for the name of the "accept" button.
**/
XULWindow.__defineGetter__("ACCEPT", function(){ return "accept"; });
/**
 * Static constant alias for the name of the "cancel" button.
**/
XULWindow.__defineGetter__("CANCEL", function(){ return "cancel"; });

With Flash CS4, Adobe moved away from the Help window and towards displaying online documentation in your browser. There was the handy Connections window trick that let you access local (offline) documentation, which was speedier to use than the documentation at adobe.com. With CS5, all help is still online, but now it’s routed to the Adobe Help application, which, honestly, is even worse than opening online documentation in your browser. And it made it much harder to find local copies of the documentation. But there is a way!

Step 1: Launch Adobe Help

I know, it’s painful, but this might be the last time you have to do it.

Step 2: Open Preferences

On the Mac, it’s under the standard location: Adobe Help > Preferences‚Ķ, or Command-Comma. On the PC, I don’t know, as I don’t have a access to a PC with CS5 installed. Maybe under Edit > Preferences?

Step 3: Go to Download Preferences

Here, you can check the applications for which you want to download help content, and, depending on the application, which sections of the help content for that application.

You’ll want to make sure Flash CS5 has a full checkmark; the default is partial content. Just go ahead and check everything that you feel you’ll be using; want speedier access to the After Effects docs? Go for it. It’s not an insignificant amount of space, but the all docs for the entire Master Collection take a total of like 430 MB of help content; not bad considering.

Then again, if you’re just after AS3 docs, you might want to check just that for now for a quicker download.

Step 4: Watch the Progress in Local Content

Local Content in the side bar doesn’t allow you to do much, just monitor what has and hasn’t downloaded. But you’ll want to have this up for the next step.

Step 5: Wait.

Things are going to download. You don’t have much control over the order in which things download, but the key is to wait for Flash CS5 to fully download. Don’t quit Adobe Help while this is going on.

Once everything is downloaded, or at least the AS3 Language and Component Reference, close the Preferences window and notice how much snappier browsing around the documentation is.

Step 6: Find the Local Files

Your files will be here:

Mac

~/Library/Preferences/chc.4875E02D9FB21EE389F73B8D1702B320485DF8CE.1/Local Store/Help/[locale]/

Windows

%appdata%\chc.4875E02D9FB21EE389F73B8D1702B320485DF8CE.1\Local Store\Help\[locale]\

I know, completely crazy preference name, right?

At this point, it’s somewhat obvious where the files are. Our interest right now is to get the AS3 Language Reference in our browser, and that is at Flash/CS5/AS3LR/index.html, relative to the platform-specific path above.

Step 7: Rock the Docs in TextMate

One last bonus step for TextMate users. Create a command that looks like this:

  • Save: Nothing
  • Command(s): echo "<meta http-equiv='Refresh' content='0;URL=file:///[path-to-your-home-folder]/Library/Preferences/chc.4875E02D9FB21EE389F73B8D1702B320485DF8CE.1/Local%20Store/Help/en_US/Flash/CS5/AS3LR/index.html'>"
  • Input: None
  • Output: Show as HTML
  • Activation: Your choice; I like Command-Option-Shift-?
  • Scope Selector: source.actionscript.3

Note: The above command has [path-to-your-home-folder] that needs to be replaced for your machine.

Also, because our current WordPress theme has trouble wrapping things, here’s the entire text of the command again:

Now, after invoking the command, you’ll get a nice WebKit rendition of the AS3 documentation, all without leaving TextMate.

That’s All

I suppose it might be a good idea to open up the Help application every now and then, just to see if there are any updates. But other than that, you can say goodbye to the Help app!

If you’ve ever tried to use fl.removeEventListener(), you’ve probably run into the following situation.

Part the First, in which you check the documentation.

It reads:

Usage

fl.removeEventListener(eventType)

Parameters

eventType
A string that specifies the event type to remove from this callback function. Acceptable values are “documentNew”, “documentOpened”, “documentClosed”, “mouseMove”, “documentChanged”, “layerChanged”, and “frameChanged”.

Returns

A Boolean value of true if the event listener was successfully removed; false if the function was never added to the list with the fl.addEventListener() method.

Description

Unregisters a function that was registered using fl.addEventListener().

Example

The following example removes the event listener associated with the documentClosed event:

fl.removeEventListener("documentClosed");

CS4 documentation can be found online here, CS5 here

Part the Second, in which You Reasonably Assume that the Code Following Would Work

fl.addEventListener("documentChanged", onDocChange);
function onDocChange() {
	fl.trace("Document changed.");
}
// elsewhere in your script...
fl.removeEventListener("documentChanged");

Part the Third, in which You Run the Code Preceding and Concordantly Receive the Error Following

The following JavaScript error(s) occurred:

At line 1 of file “/Users/dru/Desktop/tmp.jsfl”:
Wrong number of arguments passed to function “removeEventListener”.

Part the Fourth, in which You Try to Reason With the Documentation

So, then you think, “the documentation is wrong, and we need to pass the listener function in as an extra argument, that must be what this error is about.” So you try the same thing, only with this line:

fl.removeEventListener("documentChanged", onDocChange);

Which makes more sense anyway, right? But…

Part the Fifth, in which you Run the Code Preceding and Yet are Still Rewarded With Errors

The following JavaScript error(s) occurred:

At line 1 of file “/Users/dru/Desktop/tmp.jsfl”:
removeEventListener: Argument number 2 is invalid.

This seems to verify that indeed there are two parameters for fl.removeEventListener, but the second one is not the listener function.

Part the Sixth, in which You Give Up

You figure it can’t be the end of the world if you never release your event listeners…

Good news!

After filing a bug with Adobe on this, I got a response stating that the API has changed with Flash CS4, but the documentation was never updated. Adobe is, according to the email I received, working on updating the docs, but until they do, here’s the scoop.

fl.addEventListener is also mis-documented. It actually returns a numeric ID, identifying that particular event listener connection. The mysterious second parameter to fl.removeEventListener is that numeric ID. Not unlike the old setInterval/clearInterval workflow from pre-AS3 days.

So, this works:

var changeEventId = fl.addEventListener("documentChanged", onDocChange);
function onDocChange() {
	fl.trace("Document changed.");
}
// elsewhere in your script...
fl.removeEventListener("documentChanged", changeEventId);

Ta-da!

Personal aside

Honestly, this is a bit disappointing on Adobe’s part. It’s been mis-documented for two major releases of Flash Professional, not to mention that they just willy-nilly changed the API. Event listening code that works in CS3 does not work in CS4 or CS5, and vice-versa. On top of that, I feel like the numeric ID aspect is just a bit dumb. It’s supposed to be so that it can be utilized from anywhere, as long as you have the right numerical ID, and that makes sense that one script may not have a reference to the actual listener function from another script. But still. Maybe a both-techniques-work approach would be useful here. Let us pick the one that’s most convenient.

Conclusion, in which You Now Know How to Properly Use fl.removeEventListener()

You’re welcome.

OK, here’s the big project I was working on that prompted the previous two posts on XUL and the SourcePath class.

This all stemmed from trying run a project through compc to create a SWC (and asdoc, too). The problem was that quite often we make good use of the exported library symbol. And when you’re working in Flash, that’s fine; you just export the symbol and write new SomeLibrarySymbol() in your code, and you’re on your way.

But the Flex tools seem to know very little about this technique. Every time it came across SomeLibrarySymbol in my classes, it produced an error. I figured the only real solution is to create the actual class for SomeLibrarySymbol and then compc would be happy.

Of course, that’s a tedious process. Enter JSFL (of course). I wrote a script that:

  1. Presents you with a dialog to choose a source path in which to create classes
  2. Presents you with a prompt to provide a package for the new classes
  3. Loops over your selected Library items and updates the Class with the package
  4. Actually writes out class files to match the item’s class in the chosen class path

It’s ambitious, and as such I need to qualify that this has worked well for me in my one project, but use with caution.

Code is here and below. The previously published SourcePath class is required. Sorry I haven’t made an installer for this, but you’ll want to drop the class into a “lib” folder of your Commands folder, or else edit the first line of the script to point to the right class location.

fl.runScript(fl.configURI + "Commands/lib/SourcePath.jsfl");
var source = new SourcePath();

var classPathXul = "";
var classPaths = source.paths;
var iLen = classPaths.length;
for (var i = 0; i < iLen; i++) {
	var cp = classPaths[i];
	classPathXul += '		<radio id="'+(i.toString())+'" label="'+cp+'" />\n'
}

var xul = '<dialog title="Select a class path in which to create classes" buttons="accept,cancel">\n'
+ '	<radiogroup id="selection">\n'
+ 		classPathXul
+ '	</radiogroup>\n'
+ '</dialog>';

var xulFilePath = fl.configURI + "classpath.xul";

FLfile.write(xulFilePath, xul);

var classPath = fl.getDocumentDOM().xmlPanel(xulFilePath);

if (classPath.dismiss == "accept") {
	createClasses(classPath.selection);
} else {
	// Bye.
}

FLfile.remove(xulFilePath);


function createClasses(classPath) {
	var doc = fl.getDocumentDOM();
	var lib = doc.library;
	var sel = lib.getSelectedItems();
	
	fl.outputPanel.clear();
	
	var packageName = prompt("Package name: ");
	fl.trace("Chosen class path: " + classPath);
	fl.trace("Package: " + packageName);
	
	if (packageName) {
		
		// MAKE FOLDER
		var filePath = source.pathToURI(classPath) + packageName.replace(/\./g, "/") + "/";
		FLfile.createFolder(filePath);
		
		var bmdTemplate = "package " + packageName + " {\n\
\n\
	import flash.display.*;\n\
\n\
	/**\n\
	 * Linked class for library item #ITEM_NAME#.\n\
	 * @class			#CLASS_NAME#\n\
	**/\n\
	public class #CLASS_NAME# extends BitmapData {\n\
\n\
		public function #CLASS_NAME#(w:int, h:int) {\n\
			super(w, h);\n\
		}\n\
	}\n\
\n\
}\n\
"
		var spTemplate = "package " + packageName + " {\n\
\n\
	import flash.display.*;\n\
\n\
	/**\n\
	 * Linked class for library item #ITEM_NAME#.\n\
	 * @class			#CLASS_NAME#\n\
	**/\n\
	public class #CLASS_NAME# extends #BASE_CLASS# {\n\
\n\
		public function #CLASS_NAME#() {\n\
			super();\n\
		}\n\
	}\n\
\n\
}\n\
"
		var template;
		
		for (i=0; i<sel.length; i++) {
			var item = sel[i];
			switch (item.itemType) {
				case "movie clip":
					template = spTemplate;
					baseClass = "flash.display.MovieClip";
					break;
				case "bitmap":
					template = bmdTemplate;
					baseClass = "flash.display.BitmapData";
					break;
				default:
					fl.trace("Problem determining type for item " + item.name + " (type " + item.itemType + ")");
					continue;
			}
			
			var baseClass = item.linkageBaseClass == "" ? baseClass : item.linkageBaseClass;
			
			var className = item.linkageClassName;
			item.linkageClassName = packageName + "." + className;
			var as = template;
			as = as.replace(/#CLASS_NAME#/g, className);
			as = as.replace(/#BASE_CLASS#/g, baseClass);
			as = as.replace(/#ITEM_NAME#/g, item.name);
			fl.trace("Symbol: " + item.name + " - Exported as: " + packageName + "." + className);
			FLfile.write(filePath + className + ".as", as);
			
		}
	}
}

Sure makes me wish JSFL/JavaScript had some kind of heredoc mechanism.

Here’s a larger part of the bigger project of which I shared a smaller part a few days ago. I seemed to be doing a lot with the source paths (ne√© class paths), and decided to write a SourcePath class.

I don’t usually bother with classes in JSFL, as Object-Oriented JavaScript is, in my opinion, pretty painful. However, I needed this functionality in a few different scripts and went with a class as being the easiest way to share and encapsulate functionality.

Here’s a link to the documentation (created by making a stub ActionScript class and running it through asdoc). The basic functions are convenience functions. You can get the source paths as an Array of individual paths (as opposed to the normal semi-colon-separated String), you can get all source paths available to the document (both the document’s source paths combined with the application preference source paths), and you can add or remove paths more easily.

The money function is that you can get all of the paths as file URIs if desired. Assuming your working document is saved, relative source paths will get resolved to relative to the Flash document.

Here’s the class (available here, as well):

/**
 * Provides utility methods and easy access to the source paths available to a given document.
 * A SourcePath, when created, is associated with a specific Flash document.  A given SourcePath
 * object will always hold that document reference.
**/

/**
 * Creates a new SourcePath object.
 * @param	document	The document to associate with this SourcePath object.  If null, will use the currently
 * 						focused document.
**/

function SourcePath(doc) {
	
	if (!doc) doc = fl.getDocumentDOM();
	this.doc = doc;
	
	
	// PRIVATE
	function getFlaDir() {
		var flaPath    = that.doc.pathURI;
		if (!flaPath) return null;
		
		var flaPieces  = flaPath.split("/");
		flaPieces.pop()
		// This removes the hard drive name from the URI on the Mac
		flaPieces.splice(3, 1);
		var flaDir     = flaPieces.join("/");
		return flaDir;
	}
	
	var that = this;
	
	this.flaDir = getFlaDir();
	
	/**
	 * Gets an Array of Strings defining source paths set in the target document.
	**/
	this.__defineGetter__("docPaths", function(){
		return this.doc.sourcePath.split(";");
	});
	
	/**
	 * Gets an Array of Strings defining source paths set in the Flash preferences.
	**/
	this.__defineGetter__("appPaths", function(){
		return fl.sourcePath.split(";");
	});
	
	/**
	 * Gets an Array of Strings defining all source paths available to the current document, both those
	 * defined in the document and in the Flash preferences, in that order.
	**/
	this.__defineGetter__("paths", function(){
		var allPaths = this.doc.sourcePath + ";" + fl.sourcePath;
	 	return allPaths.split(";");
	});
	
	
	
	/**
	 * Returns an Array of Strings defining the source paths, as URIs, set in the target document.
	**/
	this.__defineGetter__("docPathsURI", function() {
		return this.__makeURIList(this.docPaths);
	});
	
	/**
	 * Gets an Array of Strings defining source paths, as URIs, set in the Flash preferences.
	**/
	this.__defineGetter__("appPathsURI", function() {
		return this.__makeURIList(this.appPaths);
	});
	
	/**
	 * Gets an Array of Strings defining all source paths available to the current document, both those
	 * defined in the document and in the Flash preferences, in that order.  Source paths are evaluated
	 * as URIs.
	**/
	this.__defineGetter__("pathsURI", function() {
		return this.__makeURIList(this.paths);
	});
	
	
	
	// PRIVATE
	this.__makeURIList = function(pathList) {
		var pathsURI = [];
		var iLen = pathList.length;
		var URI;
		for (var i = 0; i  pathLength) at = pathLength;
		if (at < 0) at = 0;
		
		paths.splice(at, 0, path);
		this.doc.sourcePath = paths.join(";");
	}
	/**
	 * Removes a source path from the target document by the index.
	 * @param	at	The index of the path to remove.
	**/
	this.removeDocPathAt = function(at) {
		var paths = this.docPaths;
		paths.splice(at, 1);
		this.doc.sourcePath = paths.join(";");
	}
	/**
	 * Removes a source path from the target document by name.
	 * @param	path	The source path to remove.  If not found, no changes are made.
	**/
	this.removeDocPath = function(path) {
		var paths = this.docPaths;
		var iLen = paths.length;
		var p;
		for (var i = 0; i  pathLength) at = pathLength;
		if (at < 0) at = 0;
		
		paths.splice(at, 0, path);
		fl.sourcePath = paths.join(";");
	}
	/**
	 * Removes a source path from the Flash preferences by the index.
	 * @param	at	The index of the path to remove.
	**/
	this.removeAppPathAt = function(at) {
		var paths = this.appPaths;
		paths.splice(at, 1);
		fl.sourcePath = paths.join(";");
	}
	/**
	 * Removes a source path from the Flash preferences by name.
	 * @param	path	The source path to remove.  If not found, no changes are made.
	**/
	this.removeAppPath = function(path) {
		var paths = this.appPaths;
		var iLen = paths.length;
		var p;
		for (var i = 0; i < iLen; i++) {
			p = paths[i];
			if (p == path) {
				paths.splice(i, 1);
				break;
			}
		}
		fl.sourcePath = paths.join(";");
	}
	
	
	/**
	 * Takes a source path and returns an absolute URI equivalent.  Relative source paths are evaluated against the target
	 * document's location.
	 * @param	path	The soruce path to evaluate.
	 * @return	A file URI string.  If the input path is relative, the file URI will resolve according to the target document.
	 * 			If the target document is not saved, then a relative input path will return null.
	**/
	this.pathToURI = function(path) {
		//fl.trace("flaDir: " + flaDir);
		// If flaDir is null, then we can return URI if it's an absolute path; but otherwise we can't resolve the path
		// and it's of no use anyway, so we'll return null
		//fl.trace("test: " + evaluatePath(path, flaDir));
		return evaluatePath(path, this.flaDir);
	}
	
	// PRIVATE
	function evaluatePath(classPath, flaDir) {
		// Always remove the trailing slash, if present.  We'll add it back in later.
		classPath = classPath.replace(/\/$/, "");
		if (classPath == ".") {
			if (!flaDir) return null;
			// We're on the "." class path, just concatenate the Flash file directory with the class file path.
			tryPath = flaDir + "/";
		} else if (classPath.indexOf("..") == 0) {
			if (!flaDir) return null;
			// We have a relative path that goes "up" first, so we need to remove
			var cPathPieces = classPath.split("/");
			var jLen = cPathPieces.length;
			for (var j = 0; j < jLen; j++) {
				var piece = cPathPieces[j];
				if (piece != "..") break;
			}
			cPathPieces = cPathPieces.splice(j);
			var tmpFlaPieces = flaDir.split("/");
			tmpFlaPieces = tmpFlaPieces.splice(0, tmpFlaPieces.length-j);
			tryPath = tmpFlaPieces.join("/") + "/" + cPathPieces.join("/");
			if (tryPath.search(/\/$/) == -1) {
				tryPath += "/"
			}

		} else if (classPath.indexOf("/") == 0) {
			// We have an absolute class path, so just concatenate it to the class file path.
			if (classPath.search(/\/$/) == -1) {
				classPath += "/"
			}
			tryPath = "file://" + classPath;
		} else {
			// We can assume we have a relative path that does not go "up," e.g., a "classes" folder in the same folder as the FLA.
			classPath = classPath.replace(/^\.\//, "");
			tryPath = flaDir + "/" + classPath + "/";
		}
		return tryPath;

	}
	
	
	
}

Topics

Archives

Team Members