Creating a MovieClip class for designers

Riding on what I wrote yesterday about using getter/setters for actual state control, here’s another example of how getter/setters can be used to control how your display assets work and make your life simpler.

For a while, for me, it was quite common to employ Tweener’s ColorShortcuts if I wanted to, say, tween the brightness or contrast of an image in Flash. However, there was a problem if I wanted to change the image attributes without animating it; while Tweener would let me do a transition in “0” time (applying it immediately), it’s odd that I had to use a tweening engine to do something that wasn’t related to tweening at all. Additionally, since Tweener’s color shortcuts try to apply new properties to a DisplayObject without using new variables, it was common that a special tweening would overwrite a visual effect previously applied to the object, and this is hard to solve from an external class since you don’t have a very tight control of what happens with an object.

The correct solution is having a DisplayObject class that employs all the features you want and is completely independent of the tweening engine used. This meant that, for color shortcuts, you’d ideally have a sort of a MovieClip class with attributes like brightness, contrast, etc.

I’ve used variations of this on my work for a while, but today I finally sat down and wrote a more generic display class with properties that allow anyone to adjust some of its color properties. By lack of a better name, it’s called DesignerMovieClip, and it extends MovieClip. It works exactly like the class it extends, but adds a few new properties. Here’s a basic example (that doesn’t actually change the way the object works because it’s setting the properties to the default values):

var myDMC:DesignerMovieClip = new DesignerMovieClip();
addChild(myDMC);
(...)
myDMC.saturation = 1;     // Saturation: 0 (grayscale) -> 1 (normal, default) -> 2+ (highly saturated)
myDMC.contrast = 1;       // Contrast: 0 (grey) -> 1 (normal) -> 2 (high contrast)
myDMC.brightness = 0;     // Brightness offset: -1 (black) -> 0 (normal) -> 1 (full white)
myDMC.exposure = 1;       // Brightness multiplier: 0 (black) -> 1 (normal) -> 2 (super bright)
myDMC.hue = 0;            // Hue offset in degrees: -180 -> 0 (normal) -> 180

To super-saturate an image, you’d do this:

myDMC.saturation = 2;

And obviously, you can tween it with whichever tweening engine you use. In pseudo-code, it looks like this:

SomeTweenEngine.doTween(myDMC, {saturation:2, time:1});

Internally, this class works by adding a ColorMatrixFilter that do all the color changes to the filters list. You can still set the filters manually, however – it concatenates them all before setting the object’s filters. Nothing is overwritten, and you even still access the object’s transform.colorTransform‘s properties. It’s as clean, and safe, of a solution as there can be.

An important disclaimer: this code is heavily based on Mario Klingemann’s Color Matrix class (the same code that’s used in Tweener’s color shortcuts, although the values the properties use are changed). I actually didn’t change much of his code, just stripped down to what I used, maybe changed the way some original values are treated, and then encapsulated in a self-contained display class.

The actual AS3 class is below. It’s simple and could use some improvement, but I guess it works for most cases. It also uses the stage.invalidate() “trick” to avoid re-applying the same thing every time a different display property changes.

package com.zehfernando.display {
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.filters.ColorMatrixFilter;	

	/**
	 * @author Zeh Fernando
	 */
	public class DesignerMovieClip extends MovieClip {

		// Sources:
		//
		// http://www.graficaobscura.com/matrix/index.html
		//
		// And specially Mario Klingemann's ColorMatrix class 2.1:
		// http://www.quasimondo.com/archives/000565.php
		// http://www.quasimondo.com/colormatrix/ColorMatrix.as
		// http://www.quasimondo.com
		// His code is licensed under MIT license:
		// http://www.opensource.org/licenses/mit-license.php
		

		// Static constants -------------------------------------------------------------------
		
		// Defines luminance using sRGB luminance
		protected static const LUMINANCE_R:Number = 0.212671;
		protected static const LUMINANCE_G:Number = 0.715160;
		protected static const LUMINANCE_B:Number = 0.072169;

		// Instance propertes -----------------------------------------------------------------
		
		protected var setToUpdate:Boolean;		// Whether this instance is already set to update on the next Event.RENDER
		
		// Color properties interface
		protected var _saturation:Number;		// Saturation: 0 (grayscale) -> 1 (normal, default) -> 2+ (highly saturated)
		protected var _contrast:Number;			// Contrast: 0 (grey) -> 1 (normal) -> 2 (high contrast) 
		protected var _brightness:Number;		// Brightness offset: -1 (black) -> 0 (normal) -> 1 (full white) 
		protected var _exposure:Number;			// Brightness multiplier: 0 (black) -> 1 (normal) -> 2 (super bright)
		protected var _hue:Number;				// Hue offset in degrees: -180 -> 0 (normal) -> 180 
		
		// Matrices
		protected var saturationMatrix:Array;
		protected var contrastMatrix:Array;
		protected var brightnessMatrix:Array;
		protected var exposureMatrix:Array;
		protected var hueMatrix:Array;
		
		// Overridden properties
		protected var _filters:Array;

		// ================================================================================================================
		// CONSTRUCTOR ----------------------------------------------------------------------------------------------------

		public function DesignerMovieClip() {
			super();
			setToUpdate = false;
			filters = [];
			saturation = 1;
			contrast = 1;
			brightness = 0;
			exposure = 1;
			hue = 0;
		}

		// ================================================================================================================
		// INTERNAL INSTANCE functions ------------------------------------------------------------------------------------

		protected function updateSaturationMatrix(): void {
			// Create the pre-calculated saturation matrix
			
			var nc:Number = 1-_saturation;
			var nr:Number = LUMINANCE_R * nc;
			var ng:Number = LUMINANCE_G * nc;
			var nb:Number = LUMINANCE_B * nc;

			saturationMatrix = [
				nr+_saturation,	ng,				nb,				0,	0,
				nr,				ng+_saturation,	nb,				0,	0,
				nr,				ng,				nb+_saturation,	0,	0,
				0,  			0, 				0,  			1,  0
			];
			
			requestVisualUpdate();
		}

		protected function updateContrastMatrix(): void {
			// Create the pre-calculated contrast matrix
			
			var co:Number = 128 * (1-_contrast);
			
			contrastMatrix = [
				_contrast,	0,	0, 	0, 	co,
				0,	_contrast,	0, 	0, 	co,
				0,	0,	_contrast, 	0, 	co,
				0,	0,	0, 	1, 	0
			];
			
			requestVisualUpdate();
		}

		protected function updateBrightnessMatrix(): void {
			// Create the pre-calculated brightness matrix
			
			var co:Number = 255 * _brightness;
			
			brightnessMatrix = [
				1,	0,	0, 	0, 	co,
				0,	1,	0, 	0, 	co,
				0,	0,	1, 	0, 	co,
				0,	0,	0, 	1, 	0
			];
			
			requestVisualUpdate();
		}

		protected function updateExposureMatrix(): void {
			// Create the pre-calculated exposture matrix
			
			exposureMatrix = [
				_exposure,	0,	0, 	0, 	0,
				0,	_exposure,	0, 	0, 	0,
				0,	0,	_exposure, 	0, 	0,
				0,	0,	0, 	1, 	0
			];
			
			requestVisualUpdate();
		}

		protected function updateHueMatrix(): void {
			// Create the pre-calculated hue matrix
			
			var hAngle:Number = _hue / 180 * Math.PI;
			var hCos:Number = Math.cos(hAngle);
			var hSin:Number = Math.sin(hAngle);
			
			hueMatrix = [
				((LUMINANCE_R + (hCos * (1 - LUMINANCE_R))) + (hSin * -(LUMINANCE_R))), ((LUMINANCE_G + (hCos * -(LUMINANCE_G))) + (hSin * -(LUMINANCE_G))), ((LUMINANCE_B + (hCos * -(LUMINANCE_B))) + (hSin * (1 - LUMINANCE_B))), 0, 0, 
				((LUMINANCE_R + (hCos * -(LUMINANCE_R))) + (hSin * 0.143)), ((LUMINANCE_G + (hCos * (1 - LUMINANCE_G))) + (hSin * 0.14)), ((LUMINANCE_B + (hCos * -(LUMINANCE_B))) + (hSin * -0.283)), 0, 0, 
				((LUMINANCE_R + (hCos * -(LUMINANCE_R))) + (hSin * -((1 - LUMINANCE_R)))), ((LUMINANCE_G + (hCos * -(LUMINANCE_G))) + (hSin * LUMINANCE_G)), ((LUMINANCE_B + (hCos * (1 - LUMINANCE_B))) + (hSin * LUMINANCE_B)), 0, 0, 
				0, 0, 0, 1, 0
          	 	];
			
			requestVisualUpdate();
		}
		
		protected function requestVisualUpdate(): void {
			if (Boolean(stage) && !setToUpdate) {
				setToUpdate = true;
				stage.addEventListener(Event.RENDER, onRender, false, 0, true);
				stage.invalidate();
			}
		}
		
		protected function doVisualUpdate(): void {
		 	// Create empty maytix
		 	var mtx:Array = [
		 		1,0,0,0,0,
				0,1,0,0,0,
				0,0,1,0,0,
				0,0,0,1,0
			];
			var temp:Array = [];

		 	// Precalculate a single matrix from all matrices by multiplication
		 	// The order the final matrix is calculated can change the way it looks
		 	var matrices:Array = [saturationMatrix, contrastMatrix, brightnessMatrix, exposureMatrix, hueMatrix];
		 	
		 	var i:int, j:int, mat:Array;
			var x:int, y:int;
			
			for (j = 0; j < matrices.length; j++) {
				i = 0;
				mat = matrices[j];
				for (y = 0; y < 4; y++ ) {
					
					for (x = 0; x < 5; x++ ) {
						temp[ int( i + x) ] =  Number(mat[i  ])      * Number(mtx[x]) + 
									   		   Number(mat[int(i+1)]) * Number(mtx[int(x +  5)]) + 
									   		   Number(mat[int(i+2)]) * Number(mtx[int(x + 10)]) + 
									   		   Number(mat[int(i+3)]) * Number(mtx[int(x + 15)]) +
									   		   (x == 4 ? Number(mat[int(i+4)]) : 0);
					}
					i+=5;
				}
				mtx = temp;
			}
			
			// Update object filters
			var newFilters:Array = [new ColorMatrixFilter(mtx)];
			super.filters = newFilters.concat(_filters);
		}
		
		// ================================================================================================================
		// EVENT functions ------------------------------------------------------------------------------------------------

		protected function onRender(e:Event): void {
			stage.removeEventListener(Event.RENDER, onRender);
			setToUpdate = false;
			
			doVisualUpdate();
		}

		// ================================================================================================================
		// ACCESSOR functions ---------------------------------------------------------------------------------------------

		public function get saturation(): Number {
			return _saturation;
		}
		public function set saturation(__value:Number): void {
			_saturation = __value;
			updateSaturationMatrix();
		}

		public function get contrast(): Number {
			return _contrast;
		}
		public function set contrast(__value:Number): void {
			_contrast = __value;
			updateContrastMatrix();
		}

		public function get brightness(): Number {
			return _brightness;
		}
		public function set brightness(__value:Number): void {
			_brightness = __value;
			updateBrightnessMatrix();
		}

		public function get exposure(): Number {
			return _exposure;
		}
		public function set exposure(__value:Number): void {
			_exposure = __value;
			updateExposureMatrix();
		}

		public function get hue(): Number {
			return _hue;
		}
		public function set hue(__value:Number): void {
			_hue = __value;
			updateHueMatrix();
		}

		override public function get filters(): Array {
			return _filters;
		}
		override public function set filters(__value:Array): void {
			_filters = __value;
			requestVisualUpdate();
		}
	}
}

It's sort of funny that once you start to add functionality to existing objects and to reuse them, making them simpler but more powerful, it starts to resemble the way we used to do stuff in AS1 with prototypes and such - only that now we can use actual types and things like autocompletion.

There are other variations of this kind of class I've used previously, but that's meant for future posts, I guess.

4 responses

  1. Thanks, Zeh!
    There will be always a need to apply some color transformations, so this one’s very handy. Just one note – I propose to rename this class to “ZehMovieClip”, it sounds much cooler ;]

  2. Great idea Zeh. I dig the pipeline enhancements and making the developer to designer flow optimized and straight forward. This is right along those lines much like standardized simple animation kits. One day I know you will make that 3d editor with effects and people will be making 3d canvas compositions and animating them, this from your original bezier in papervision tween. Maybe when school is up I will have to do it… I like making editors, tools that the designer can edit content right in the content. Imagine using views to layout 3d stuff for pv3d, away etc then being able use your visual timeline stuff to devise scenes and have mutiple views like 3dsmax or maya. Ok I ramble, just more time!

  3. Hello Fernando,
    Many thanks for your great work!
    Currently, i’m using the tweener class and i’ve a problem.. i need to make a change of velocity between two or more points in an array..( i’ve an aircraft flying along this array) ..So imagine that this flight will crash with another and i need to change the velocity at soe point in the route…o later in other point back to the normal velocity…is it possible?
    Thanks!!!

Comments are closed.