Wednesday, July 15, 2009

Color manipulation in Flex (HSL class +)

Flex has some great color manipulation tools built in. The ColorMatrixFilter class is amazing, particularly when paired with the ColorMatrix class found here:
http://www.quasimondo.com/archives/000599.php

However, for my current project, I've also needed the ability to recolor specific RGB values. The reason is simple: I'm not necessarily using a filter, but am in some cases changing CSS values on the fly. Let's say the DataGrid is defined like so in CSS:


DataGrid {
borderColor:#1b3244;
rollOverColor: #3c5c80;
selectionColor: #bacfe7;
...


Now let's say I want to change borderColor in the DataGrid to an arbitrary color and have all the other colors change along with it, but maintain their relationships to the first color (e.g., rollOverColor is somewhat lighter, selectionColor is much lighter), et cetera.

To solve this problem, I made two classes, the first based on the great C# project found here:
http://www.codeproject.com/KB/recipes/colorspace1.aspx


public class HslColor
{
/**
* adapted from:
* http://www.codeproject.com/KB/recipes/colorspace1.aspx
*
* A simple class to convert between RGB and HSL values. This can be constructed with a RGB value,
* HSL values may be extracted and manipulated, and the result can be returned as a RGB value.
*/

/**
* 0 to 359
*/
public var h:uint;

/**
* 0 to 1
*/
public var s:Number;

/**
* 0 to 1
*/
public var l:Number;

/**
* Construct a HslColor from a rgb value. The h, s, l values can be manipulated, and the rgb
* value can be then retreived using getRgbColor().
*/
public function HslColor(color:uint)
{
rgb = color;
}

static public function fromString(color:String):HslColor
{
return new HslColor(uint(color.replace("#", "0x")));
}

static public function toRGB(h:Number, s:Number, l:Number):uint
{
var hslColor:HslColor = new HslColor(0);
hslColor.h = h;
hslColor.s = s;
hslColor.l = l;
return hslColor.rgb;
}

public function get rgb():uint
{
var r:uint =0;
var g:uint = 0;
var b:uint = 0;

if(s == 0)
{
r = g = b = l * 255;
}
else
{
var q:Number = (l<0.5)?(l * (1.0+s)):(l+s - (l*s));
var p:Number = (2.0 * l) - q;

var Hk:Number = h/360.0;
var T:Array = new Array(3);
T[0] = Hk + (1.0/3.0); // Tr
T[1] = Hk; // Tb
T[2] = Hk - (1.0/3.0); // Tg

for(var i:int = 0; i < 3; i++)
{
if(T[i] < 0) T[i] += 1.0;
if(T[i] > 1) T[i] -= 1.0;

if((T[i]*6) < 1)
{
T[i] = p + ((q-p)*6.0*T[i]);
}
else if((T[i]*2.0) < 1)
{
T[i] = q;
}
else if((T[i]*3.0) < 2)
{
T[i] = p + (q-p) * ((2.0/3.0) - T[i]) * 6.0;
}
else T[i] = p;
}

r = uint(255 * T[0]);
g = uint(255 * T[1]);
b = uint(255 * T[2]);
}

return (r << 16) | (g << 8) | b;
}

public function set rgb(color:uint):void
{
var r:Number = color >> 16 & 0xFF;
var g:Number = color >> 8 & 0xFF;
var b:Number = color & 0xFF;

h = 0;
s = l = 0;

// normalizes red-green-blue values
var rFraction:Number = Number(r/255);
var gFraction:Number = Number(g/255);
var bFraction:Number = Number(b/255);

var max:Number = Math.max(rFraction, Math.max(gFraction, bFraction));
var min:Number = Math.min(rFraction, Math.min(gFraction, bFraction));

// h
if(max == min)
{
h = 0; // undefined
}
else if (max == rFraction && gFraction >= bFraction)
{
h = 60.0 * (gFraction - bFraction) / (max - min);
}
else if (max == rFraction && gFraction < bFraction)
{
h = 60.0 * (gFraction - bFraction) / (max - min) + 360.0;
}
else if (max == gFraction)
{
h = 60.0 * (bFraction - rFraction) / (max - min) + 120.0;
}
else if (max == bFraction)
{
h = 60.0 * (rFraction - gFraction) / (max - min) + 240.0;
}

// luminance
l = (max + min) / 2.0;

// s
if(l == 0 || max == min)
{
s = 0;
}
else if (0 < l && l <= 0.5)
{
s = (max - min) / (max + min);
}
else if (l > 0.5)
{
s = (max - min) / (2 - (max + min)); //(max-min > 0)?
}
}
}


This is a very useful little class. You can create one with a RGB value passed in and read its HSL(hue, saturation and lightness) properties. What's more, you can modify the h, s or l properties, and get the rgb property.

For example, you could desaturate a color as follows:


var hslColor:HslColor = new HslColor(rgb);
hslColor.s *= .5;
rgb = hslColor.rgb;


Going back to the original example, how do I change the DataGrid.borderColor to an arbitrary color and have all the other color values change correspondingly?

Well, with the aid of the HslColor class and the following ColorUtils class I wrote:

public class ColorUtils
{
/**
* Return a color derived from baseColor using sampleStartColor and sampleEndColor as a guide.
* For example, this function would return dark blue if baseColor was light blue, sampleStartColor
* was pink and sampleEndColor was dark red.
*
*/
public static function calculateDerivedColor(baseColor:uint, sampleStartColor:uint, sampleEndColor:uint):uint
{
var baseColorHSL:HslColor = new HslColor(baseColor);

var sampleStartColorHSL:HslColor = new HslColor(sampleStartColor);
var sampleEndColorHSL:HslColor = new HslColor(sampleEndColor);

// rotate the hue and offset the saturation and lightness based on the inputs
baseColorHSL.h = (baseColorHSL.h + sampleEndColorHSL.h - sampleStartColorHSL.h + 360) % 360;
baseColorHSL.s = Math.min(1,Math.max(0, baseColorHSL.s + sampleEndColorHSL.s - sampleStartColorHSL.s));
baseColorHSL.l = Math.max(0, baseColorHSL.l + sampleEndColorHSL.l - sampleStartColorHSL.l);

return baseColorHSL.rgb;
}

}


Here calculateDerivedColor() returns a new color based on baseColor but with the differences between sampleStartColor and sampleEndColor applied to it. So if sampleStartColor and sampleEndColor were the same color, it should simply return baseColor.

To set DataGrid's borderColor to an arbitrary color and set rollOverColor and selectionColor to a related color, we can do the following:


var datagridCSS:CSSStyleDeclaration = StyleManager.getStyleDeclaration("DataGrid");
datagridCSS.setStyle('borderColor', newBorderColor);
datagridCSS.setStyle('rollOverColor', ColorUtils.calculateDerivedColor(newBorderColor, 0x1b3244, 0x3c5c80));
datagridCSS.setStyle('selectionColor', ColorUtils.calculateDerivedColor(newBorderColor, 0x1b3244, 0xbacfe7));


Note that 0x1b3244 was the original borderColor (which we replaced with newBorderColor) and 0x3c5c80 and 0xbacfe7 were the original rollOverColor and selectionColor values, respectively.

Feel free to use this code however you see fit. A link back here would be appreciated.

1 comment: