Silverlight Brass Tacks

Bill Reiss' Silverlight Ramblings
My upcoming Silverlight book for beginners Hello! Silverlight 2 with Dave Campbell, available online now!



Pages

Recent posts

Navigation

Archive

Blogroll

Tampa Divorce Lawyer

North of Tampa in Lutz, Florida. A Tampa Divorce Lawyer focusing on family, divorce, and real estate law.

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

HSL Colors in Silverlight

Source code: http://www.bluerosegames.com/hslcolorsample.zip

On a recent project (showing up soon on Blue Rose Games) I had the need to create some objects that were identical except for their color. So I created a UserControl for the object, and gave it a property to let me select the color. The problem is, this object uses gradients to get the desired effect, and the color of the gradient stops was a variation of some base color, keeping the color consistent but making it lighter or darker.

If you've ever done work with color, possibly in Photoshop or something similar, you know that adjusting the lightness of a color using the typical Red, Green, and Blue color components is difficult. Photoshop typically converts the color to HSL (standing for Hue, Saturation, and Lightness) and then changes the Lightness value, leaving the Hue and Saturation consistent.

So I did a quick search on the web and found that the formulas for converting back and forth between RBG color and HSL color were on Wikipedia at http://en.wikipedia.org/wiki/HSV_color_space. Without too much work, I was able to create an HslColor struct which could convert values back and forth between RGB and HSL. This is the code:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
 
namespace HslColorSample
{
    public struct HslColor
    {
        // value from 0 to 1 
        public double A;
        // value from 0 to 360 
        public double H;
        // value from 0 to 1 
        public double S;
        // value from 0 to 1 
        public double L;
 
        private static double ByteToPct(byte v)
        {
            double d = v;
            d /= 255;
            return d;
        }
 
        private static byte PctToByte(double pct)
        {
            pct *= 255;
            pct += .5;
            if (pct > 255) pct = 255;
            if (pct < 0) pct = 0;
            return (byte)pct;
        }
 
        public static HslColor FromColor(Color c)
        {
            return HslColor.FromArgb(c.A, c.R, c.G, c.B);
        }
 
        public static HslColor FromArgb(byte A, byte R, byte G, byte B)
        {
            HslColor c = FromRgb(R, G, B);
            c.A = ByteToPct(A);
            return c;
        }
 
        public static HslColor FromRgb(byte R, byte G, byte B)
        {
            HslColor c = new HslColor();
            c.A = 1;
            double r = ByteToPct(R);
            double g = ByteToPct(G);
            double b = ByteToPct(B);
            double max = Math.Max(b, Math.Max(r, g));
            double min = Math.Min(b, Math.Min(r, g));
            if (max == min)
            {
                c.H = 0;
            }
            else if (max == r && g >= b)
            {
                c.H = 60 * ((g - b) / (max - min));
            }
            else if (max == r && g < b)
            {
                c.H = 60 * ((g - b) / (max - min)) + 360;
            }
            else if (max == g)
            {
                c.H = 60 * ((b - r) / (max - min)) + 120;
            }
            else if (max == b)
            {
                c.H = 60 * ((r - g) / (max - min)) + 240;
            }
 
            c.L = .5 * (max + min);
            if (max == min)
            {
                c.S = 0;
            }
            else if (c.L <= .5)
            {
                c.S = (max - min) / (2 * c.L);
            }
            else if (c.L > .5)
            {
                c.S = (max - min) / (2 - 2 * c.L);
            }
            return c;
        }
 
        public HslColor Lighten(double pct)
        {
            HslColor c = new HslColor();
            c.A = this.A;
            c.H = this.H;
            c.S = this.S;
            c.L = Math.Min(Math.Max(this.L + pct, 0), 1);
            return c;
        }
 
        public HslColor Darken(double pct)
        {
            return Lighten(-pct);
        }
 
        private double norm(double d)
        {
            if (d < 0) d += 1;
            if (d > 1) d -= 1;
            return d;
        }
 
        private double getComponent(double tc, double p, double q)
        {
            if (tc < (1.0 / 6.0))
            {
                return p + ((q - p) * 6 * tc);
            }
            if (tc < .5)
            {
                return q;
            }
            if (tc < (2.0 / 3.0))
            {
                return p + ((q - p) * 6 * ((2.0 / 3.0) - tc));
            }
            return p;
        }
 
        public Color ToColor()
        {
            double q = 0;
            if (L < .5)
            {
                q = L * (1 + S);
            }
            else
            {
                q = L + S - (L * S);
            }
            double p = (2 * L) - q;
            double hk = H / 360;
            double r = getComponent(norm(hk + (1.0 / 3.0)), p, q);
            double g = getComponent(norm(hk), p, q);
            double b = getComponent(norm(hk - (1.0 / 3.0)), p, q);
            return Color.FromArgb(PctToByte(A), PctToByte(r), PctToByte(g), PctToByte(b));
        }
    }
}

 

The nice thing is though that you don't really have to know how this works to use it. All you have to know about is the FromColor and ToColor methods. Let's say I want to create a glassy ball (which, conveniently, is what I needed for my project). First I went into Blend and created an approximation of how I wanted it to look, not worrying too much about the colors since those will be replaced in code anyway, but getting the gradient stops in the right places:

<UserControl x:Class="HslColorSample.GlassyMarble"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Width="110" Height="110">
    <Grid x:Name="PegRoot" Width="110" Height="110" Visibility="Visible" d:IsHidden="True">
            <Ellipse Width="110" Height="110" RenderTransformOrigin=".5,.5">
                <Ellipse.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform ScaleX="1.1" ScaleY="1.2"/>
                        <RotateTransform Angle="-5"/>
                        <TranslateTransform X="5" Y="15"/>
                    </TransformGroup>
                </Ellipse.RenderTransform>
                <Ellipse.Fill>
                    <RadialGradientBrush>
                        <GradientStop Offset=".7" Color="#A0000000"/>
                        <GradientStop Offset=".98" Color="#00000000"/>
                    </RadialGradientBrush>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse Width="100" Height="100">
                <Ellipse.Fill>
                    <RadialGradientBrush Center=".47,.4">
                        <GradientStop x:Name="stop1" Offset="0" Color="#FF9999FF"/>
                        <GradientStop x:Name="stop2" Offset=".55" Color="#FF4444FF"/>
                        <GradientStop x:Name="stop3" Offset=".7" Color="#FF2222FF"/>
                        <GradientStop x:Name="stop4" Offset=".8" Color="#FF0000FF"/>
                        <GradientStop x:Name="stop5" Offset="1" Color="#FF000080"/>
                    </RadialGradientBrush>
                </Ellipse.Fill>
            </Ellipse>
 
            <Ellipse Width="65" Height="40" Margin="0,-50,0,0">
                <Ellipse.Fill>
                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                        <GradientStop Offset="0" Color="#CCFFFFFF"/>
                        <GradientStop Offset=".9" Color="#33FFFFFF"/>
                    </LinearGradientBrush>
                </Ellipse.Fill>
            </Ellipse>
        </Grid>
</UserControl>

This already gives a pretty nice visual, like this:

marble

In the XAML, I have named the gradient stops for the color of the marble so that they can be easily accessed from code. This section of the XAML looks like this:

<RadialGradientBrush Center=".47,.4">
    <GradientStop x:Name="stop1" Offset="0" Color="#FF9999FF"/>
    <GradientStop x:Name="stop2" Offset=".55" Color="#FF4444FF"/>
    <GradientStop x:Name="stop3" Offset=".7" Color="#FF2222FF"/>
    <GradientStop x:Name="stop4" Offset=".8" Color="#FF0000FF"/>
    <GradientStop x:Name="stop5" Offset="1" Color="#FF000080"/>
</RadialGradientBrush>

Now I want stop4 to be the base color, the color that is exposed as a property. All of the other colors will derive from this. For this we'll add a public Color property to the GlassyMarble:

private Color color;
 
public Color Color
{
    set
    {
        color = value;
        HslColor hslBase = HslColor.FromColor(color);
        stop4.Color = color;
        stop5.Color = hslBase.Darken(.2).ToColor();
        stop3.Color = hslBase.Lighten(.05).ToColor();
        stop2.Color = hslBase.Lighten(.1).ToColor();
        stop1.Color = hslBase.Lighten(.4).ToColor();
    }
    get
    {
        return color;
    }
}

Note that the color passed in is converted to an HslColor, and then we use the Lighten and Darken methods to adjust the Lightness value of the color. Then we convert it back into a Color so that Silverlight can use it.

Now if we specify the color of the marble:

<my:GlassyMarble Color="#FF0000BB"/>

We get something that looks like this:

hslmarble

And if we change the color to something else, like a dark red:

<my:GlassyMarble Color="DarkRed"/>

We get all of the other colors changed appropriately:

redmarble

Here's a few in a StackPanel:

<StackPanel x:Name="LayoutRoot" Background="White" Orientation="Horizontal">
    <my:GlassyMarble Color="#FF0000BB"/>
    <my:GlassyMarble Color="DarkRed"/>
    <my:GlassyMarble Color="DarkGreen"/>
    <my:GlassyMarble Color="Orange"/>
</StackPanel>

And the corresponding display:

marbles

There are things you can do with HSL besides adjusting Lightness, for example you can use variations of hue to find colors that go together for a page, such as complementary colors, or you could vary the saturation, for more information search on art color theory, and you'll get some results like this:

http://www.colormatters.com/colortheory.html

http://en.wikipedia.org/wiki/Color_theory

http://www.worqx.com/color/

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList
Posted: May 03 2008, 08:33 by Bill Reiss | Comments (7) RSS comment feed |
  • Currently 4.25/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under:

Related posts

Comments

Gary us said:

GaryThis is a great way to implement HSL. I'm just starting to think of all the ways that I can use this.

Thanks.

-Gary

# May 03 2008, 08:51

Utah Movers se said:

Utah Moversthank u
great post
made my day

# April 21 2009, 11:50

Colorado Movers vn said:

Colorado Moversmade me smile
love it sir
great post

# April 21 2009, 11:51

jammer ch said:

jammergreat post
love u all

# April 27 2009, 11:17

Maryland movers ve said:

Maryland moverslove u
great post

# April 27 2009, 11:20

Denver Movers vn said:

Denver Moversgreat post
thank u very much

# April 27 2009, 11:21

sohbet said:

sohbetthank u
great post
love u all

# April 29 2009, 06:54