HSL Color for WPF, and Markup Extension to Lighten/Darken Colors in XAML

When developing WPF applications with custom visual design, it is very convenient to have a way to specify lighter or darker shades of the same base color. One obvious application of this is setting colors for hovered and pressed states of a button. Here is my solution to this problem.

First, we need to a way to separately control HSL components of color. Here is HslColor class:


public class HslColor
{
	public readonly double h, s, l, a;

	public HslColor(double h, double s, double l, double a)
	{
		this.h = h;
		this.s = s;
		this.l = l;
		this.a = a;
	}

	public HslColor(System.Windows.Media.Color rgb)
	{
		RgbToHls(rgb.R, rgb.G, rgb.B, out h, out l, out s);
		a = rgb.A / 255.0;
	}

	public System.Windows.Media.Color ToRgb()
	{
		int r, g, b;
		HlsToRgb(h, l, s, out r, out g, out b);
		return System.Windows.Media.Color.FromArgb((byte)(a * 255.0), (byte)r, (byte)g, (byte)b);
	}

	public HslColor Lighten(double amount)
	{
		return new HslColor(h, s, Clamp(L * amount, 0, 1), a);
	}

	private static double Clamp(double value, double min, double max)
	{
		if (value < min)
			return min;
		if (value > max)
			return max;

		return value;
	}

	// Convert an RGB value into an HLS value.
	static void RgbToHls(int r, int g, int b,
		out double h, out double l, out double s)
	{
		// Convert RGB to a 0.0 to 1.0 range.
		double double_r = r / 255.0;
		double double_g = g / 255.0;
		double double_b = b / 255.0;

		// Get the maximum and minimum RGB components.
		double max = double_r;
		if (max < double_g) max = double_g;
		if (max < double_b) max = double_b;

		double min = double_r;
		if (min > double_g) min = double_g;
		if (min > double_b) min = double_b;

		double diff = max - min;
		l = (max + min) / 2;
		if (Math.Abs(diff) < 0.00001)
		{
			s = 0;
			h = 0;  // H is really undefined.
		}
		else
		{
			if (l <= 0.5) s = diff / (max + min);
			else s = diff / (2 - max - min);

			double r_dist = (max - double_r) / diff;
			double g_dist = (max - double_g) / diff;
			double b_dist = (max - double_b) / diff;

			if (double_r == max) h = b_dist - g_dist;
			else if (double_g == max) h = 2 + r_dist - b_dist;
			else h = 4 + g_dist - r_dist;

			h = h * 60;
			if (h < 0) h += 360;
		}
	}

	// Convert an HLS value into an RGB value.
	static void HlsToRgb(double h, double l, double s,
		out int r, out int g, out int b)
	{
		double p2;
		if (l <= 0.5) p2 = l * (1 + s);
		else p2 = l + s - l * s;

		double p1 = 2 * l - p2;
		double double_r, double_g, double_b;
		if (s == 0)
		{
			double_r = l;
			double_g = l;
			double_b = l;
		}
		else
		{
			double_r = QqhToRgb(p1, p2, h + 120);
			double_g = QqhToRgb(p1, p2, h);
			double_b = QqhToRgb(p1, p2, h - 120);
		}

		// Convert RGB to the 0 to 255 range.
		r = (int)(double_r * 255.0);
		g = (int)(double_g * 255.0);
		b = (int)(double_b * 255.0);
	}

	private static double QqhToRgb(double q1, double q2, double hue)
	{
		if (hue > 360) hue -= 360;
		else if (hue < 0) hue += 360;

		if (hue < 60) return q1 + (q2 - q1) * hue / 60;
		if (hue < 180) return q2;
		if (hue < 240) return q1 + (q2 - q1) * (240 - hue) / 60;
		return q1;
	}
}

Then, we need a markup extension to lighten and darken a specified color:


[MarkupExtensionReturnType(typeof(SolidColorBrush))]
public class LightenExtension : MarkupExtension
{
	public SolidColorBrush Source { get; set; }
	public double Amount { get; set; }

	public override object ProvideValue(IServiceProvider serviceProvider)
	{
		return new SolidColorBrush(new HslColor(Source.Color).Lighten(Amount).ToRgb());
	}
}

Here is how to use it to lighten background on hover:


<SolidColorBrush x:Key="BackgroundBrush" Color="Red" />
...
			<Trigger Property="Border.IsMouseOver" Value="true">
				<Setter TargetName="Border" Property="Background" Value="{color:Lighten Source={StaticResource BackgroundBrush}, Amount=1.3}" />
			</Trigger>

You can also darken colors by providing a value less than 1 for amount, e.g.


{color:Lighten Source={StaticResource BackgroundBrush}, Amount=0.7}

A similar technique can be used to lighten and darken data bound colors by introducing a value converter:


public class LightenConverter : IValueConverter
{
	public double Amount { get; set; }

	public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
	{
		var source = (SolidColorBrush)value;
		return new SolidColorBrush(new HslColor(source.Color).Lighten(Amount).ToRgb());
	}

	public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
	{
		throw new NotImplementedException();
	}
}

And the usage is:


<Resources>
    <color:LightenConverter x:Key="DarkenConverter" Amount="0.65" />
</Resources>

....

<... Foreground="{Binding SomeProperty, Converter={StaticResource DarkenConverter}}">

RGB to HSL conversion credits go to this article.