Silverlight Brass Tacks

Bill Reiss' Silverlight Ramblings

About the author

Author Name is someone.
E-mail me Send mail

Recent comments

Authors

Tags

Don't show

    Categories

    None


    Disclaimer

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

    © Copyright 2008

    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/

    Currently rated 5.0 by 3 people

    • Currently 5/5 Stars.
    • 1
    • 2
    • 3
    • 4
    • 5

    Posted by Bill Reiss on Saturday, May 03, 2008 5:33 AM
    Permalink | Comments (1) | Post RSSRSS comment feed