03 July 2006

I searched the web for a while to find a WPF HLS Color selector like Interactive Designer is equipped with.

Since I didn’t find anything I thought about creating my own one.

The most difficult task was to convert .NET Rgb Color Values to HLS values (the difference is explained here). In my opinion the HLS color system is superior to the RGB system. After hours of searching I found a VBA sample and ported it to WPF (the code can be found at the bottom of the article).

The next thing to do was to create a new slider for the Hue and a triangle for Lightness and Saturation values.

The Slider

It was easy to create a Color Slider since it is possible to override the default template. I created a gradient from hue 0 over hue 0.1, hue 0.2 … to hue 1.0 and put it into the background. At last I restyled the value selection thumb using Interactive Designer and the result looked like this.

The Triangle

The triangle was a bit more complex than the slider since I had to start from scratch. I created the triangle shape using Paths and Interactive Designer. The I assigned 3 backgrounds with different opacity masks to it.

1. Background Black From lower left to upper right

Background White From upper left to lower right

Background Colored HLS(CurrentHue, 0.5, 1) from right to left

To show the current selected Lightness and Saturation I added an ellipse.

The next step was to attach MouseDown, MouseUp and MouseMove event Handlers to the triangle. Whenever the mouse is moved over the triangle with the left button pressed the ellipse should follow. To ensure that the ellipse is moved correctly when you leave the triangle with the left button pressed I assigned Mouse.Capture(element, Element) to the element on MouseDown and Mouse.Capture(element, None) on MouseUp.

Last but not least I had to write the code that generates the appropriate color from slider and ellipse position. The complete code can be found here.

The Result

In my opinion it was worth to spent some time to create this control. I hope that more people will begin building their own controls and make them available to the public. My control is not perfect and could be done in other ways. This is article is supposed to show only one possible way.

Rgb <–> Hls Converter Code

Imports System.Windows.Media

Public Class HlsConverter

Public Shared Function ConvertFromHls(ByVal value As HlsValue) As Color

Return ConvertFromHls(value.Hue, value.Lightness, value.Saturation)

End Function

Public Shared Function ConvertFromHls(ByVal h As Double, ByVal l As Double, ByVal s As Double) As Color

Dim p1 As Double

Dim p2 As Double

Dim r As Double

Dim g As Double

Dim b As Double

h *= 360

If l <= 0.5 Then

p2 = l * (1 + s)

Else

p2 = l + s - l * s

End If

p1 = 2 * l - p2

If s = 0 Then

r = l

g = l

b = l

Else

r = QqhToRgb(p1, p2, h + 120)

g = QqhToRgb(p1, p2, h)

b = QqhToRgb(p1, p2, h - 120)

End If

r *= Byte.MaxValue

g *= Byte.MaxValue

b *= Byte.MaxValue

Return Color.FromRgb(CByte(r), CByte(g), CByte(b))

End Function

Public Shared Function QqhToRgb(ByVal q1 As Double, ByVal q2 As _

Double, ByVal hue As Double) As Double

If hue > 360 Then

hue = hue - 360

ElseIf hue < 0 Then

hue = hue + 360

End If

If hue < 60 Then

QqhToRgb = q1 + (q2 - q1) * hue / 60

ElseIf hue < 180 Then

QqhToRgb = q2

ElseIf hue < 240 Then

QqhToRgb = q1 + (q2 - q1) * (240 - hue) / 60

Else

QqhToRgb = q1

End If

End Function

Public Shared Function ConvertToHls(ByVal color As Color) As HlsValue

Dim max As Double

Dim min As Double

Dim diff As Double

Dim r_dist As Double

Dim g_dist As Double

Dim b_dist As Double

Dim r As Double

Dim g As Double

Dim b As Double

Dim h As Double

Dim l As Double

Dim s As Double

r = color.R / Byte.MaxValue

g = color.G / Byte.MaxValue

b = color.B / Byte.MaxValue

max = R

If max < G Then max = G

If max < B Then max = B

min = R

If min > G Then min = G

If min > B Then min = B

diff = max - min

L = (max + min) / 2

If Math.Abs(diff) < 0.00001 Then

s = 0

h = 0

Else

If l <= 0.5 Then

s = diff / (max + min)

Else

s = diff / (2 - max - min)

End If

r_dist = (max - r) / diff

g_dist = (max - g) / diff

b_dist = (max - b) / diff

If r = max Then

h = b_dist - g_dist

ElseIf g = max Then

h = 2 + r_dist - b_dist

Else

h = 4 + g_dist - r_dist

End If

h = h * 60

If h < 0 Then h = h + 360

End If

h /= 360

Return New HlsValue(h, l, s)

End Function

End Class

Public Structure HlsValue

Private mHue As Double

Private mLightness As Double

Private mSaturation As Double

Public Sub New(ByVal h As Double, ByVal l As Double, ByVal s As Double)

mHue = h

mLightness = l

mSaturation = s

End Sub

Public Property Hue() As Double

Get

Return mHue

End Get

Set(ByVal value As Double)

mHue = value

End Set

End Property

Public Property Lightness() As Double

Get

Return mLightness

End Get

Set(ByVal value As Double)

mLightness = value

End Set

End Property

Public Property Saturation() As Double

Get

Return mSaturation

End Get

Set(ByVal value As Double)

mSaturation = value

End Set

End Property

Public Shared Operator <>(ByVal left As HlsValue, ByVal right As HlsValue) As Boolean

Dim bool1 As Boolean

bool1 = left.Lightness <> right.Lightness Or left.Saturation <> right.Saturation

If Not bool1 Then

Return left.Hue <> right.Hue And (left.Hue > 0 And right.Hue < 1) And (left.Hue < 1 And right.Hue > 0)

Else

Return bool1

End If

End Operator

Public Shared Operator =(ByVal left As HlsValue, ByVal right As HlsValue) As Boolean

Dim bool1 As Boolean

bool1 = left.Lightness = right.Lightness And left.Saturation = right.Saturation

If bool1 Then

Return Math.Round(left.Hue, 2) = Math.Round(right.Hue, 2) Or (Math.Round(left.Hue, 2) = 1 And Math.Round(right.Hue, 2) = 0) Or (Math.Round(left.Hue, 2) = 0 And Math.Round(right.Hue, 2) = 1)

Else

Return False

End If

End Operator

Public Overrides Function ToString() As String

Return String.Format("H: {0:0.00} L: {1:0.00} S: {2:0.00}", Hue, Lightness, Saturation)

End Function

End Structure