Saturday, September 26, 2015

Silverlight and Color

A while back I was looking for a decent color picker for Silverlight - one that didn't require me to actually use an image for the colors since that isn't editable by the user.

I had looked into what it took to create custom colors and brushes dynamically. It was mainly because a client had asked about being able to choose custom colors for a theme. At that time I couldn't figure anything out, however the current project I am on gave me a bit more insight on how I could possibly make the brushes a bit more interactive and dynamic.

Note: the goal was to be able to dynamically change the color without going through a theme, without choosing from a bitmap image and to show the actual color value that reflects the updated color more accurately. So, instead of creating several brushes in a resource dictionary it uses the mathematical color formula (for the HSL color space in this project). 



The linear gradient brush is not scanned in, but created as the app loops through the formulas, using the index value as the hue value, and creates the colors based it and the formulas. 

Most of the formulas came from:
Math behind colorspaces
Math stack exchange
HSL and HSV

I actually found using a Tuple with three items very helpful in calculating the colors

Tuple<double,double,double> rgbColorValues(double hue, double lum, double sat)
        {
 
            double lumValue = lum / 100;
 
            double temp_One = 0;
 
            temp_One = luminousValue(lum, sat);
 
            double temp_Two;
            temp_Two = 2 * lumValue - temp_One;
 
 
            double temp_Red = 0;
            double temp_Green = 0;
            double temp_Blue = 0;
 
            temp_Red = colorTest(hue, "red");
            temp_Green = colorTest(hue, "green");
            temp_Blue = colorTest(hue, "blue");
 
            double colorRed = 0;
            double colorGreen = 0;
            double colorBlue = 0;
 
            colorRed = colorConversion(temp_One, temp_Two, temp_Red);
            colorGreen = colorConversion(temp_One, temp_Two, temp_Green);
            colorBlue = colorConversion(temp_One, temp_Two, temp_Blue);
 
            return Tuple.Create(colorRed, colorGreen, colorBlue);
 
        }

I admit, it should be lightness and I have luminous as the variable, but I am a bit more concerned with getting it to work at the moment :) - and this helps a lot. It brings in the three values used to calculate the color's value: hue (angle), saturation and lightness. Hue is 0-360 while saturation and lightness are both 0-100.

Currently the issue I have is when the Lightness is 50 or higher. I did find a formula that should fix it, however - getting it into the code is challenging without accidentally breaking something - but here is what I currently have that works well for below 50 (and used in the code above)

public double luminousValue(double lumNumber, double satNumber)
        {
            double lumValue = lumNumber / 100;
            double satValue = satNumber / 100;
 
            double tempNumber;
 
            if(lumValue <= 0.5)
            {
                tempNumber = lumValue * (1 + satValue);
            }
            else
            {
                tempNumber = (lumValue + satValue) - (lumValue * satValue);
            }
 
            return tempNumber;
        }


While the hue is the same for all the colors, the difference is the formula used to calculate what the value is for Red, Green and Blue. Luckily a nice little switch statement helps with this:

public double colorTest(double hue, string color)
        {
            double hueValue = hue / 360;
            string colorName = color;
            double tempColor;
            double colorValue;
 
            switch(colorName)
            {
                case"red":
                    tempColor = hueValue + 0.333;
                    break;
                case"green":
                    tempColor = hueValue;
                    break;
                case"blue":
                    tempColor = hueValue - 0.333;
                    break;
                default:
                    tempColor = hueValue;
                    break;
            }
 
            if (tempColor < 0)
            {
                colorValue = tempColor + 1;
            }
            else if(tempColor > 1)
            {
                colorValue = tempColor - 1;
            }
            else
            {
                colorValue = tempColor;
            }
 
            return colorValue;
        }


And the actual conversion is dependent on the value of a certain calculations, and I found putting it in a separate method helped me too:

public double colorConversion(double tempOne, double tempTwo, double colorValue)
        {
 
            double convertedColor;
 
            if(6*colorValue <1.0)
            {
                convertedColor = Math.Round(255*(tempTwo + (tempOne - tempTwo) * 6 * colorValue),2);  
            }
            else if(2*colorValue<1)
            {
                convertedColor = Math.Round(255*tempOne,2);
            }
            else if(3*colorValue<2)
            {
                convertedColor = Math.Round(255*(tempTwo + (tempOne - tempTwo) * (0.667 - colorValue) * 6),2);
            }
            else
            {
                convertedColor = Math.Round(255*tempTwo,2);
            }
 
            return convertedColor;
 
        }

Here is where the brush is built - and a list for the values that gives me access to both the hex value and location in the gradient brush. By creating the list the same time I create the brush - and they are both tied to the value of the hue, I can call up the color value of a specific area in the gradient brush by calling up the corresponding index of the color list. If the value of the hue is 30, it pulls the value from the item with an index of 30 in the list.

public void buildColorDefs()
        {
            LinearGradientBrush colorRangeBrush = new LinearGradientBrush();
            colorRangeBrush.StartPoint = new Point(0, 0);
            colorRangeBrush.EndPoint = new Point(1, 1);
            colorRangeBrush.MappingMode = BrushMappingMode.RelativeToBoundingBox;
 
            IList colorStops = colorRangeBrush.GradientStops;
 
            for (int i = 0; i < 360; i++)
            {
                TextBlock textBlock = new TextBlock();
                textBlock.Text = convertFromHue(i).ToString();
                textBlock.VerticalAlignment = VerticalAlignment.Center;
                textBlock.HorizontalAlignment = HorizontalAlignment.Center;
 
 
                string hexColor = convertFromHue(i).ToString();
                hexColor = hexColor.Replace("#"string.Empty);
 
                SolidColorBrush fillBrush = new SolidColorBrush(
                    Color.FromArgb(
                    Convert.ToByte(hexColor.Substring(0, 2), 16),
                    Convert.ToByte(hexColor.Substring(2, 2), 16),
                    Convert.ToByte(hexColor.Substring(4, 2), 16),
                    Convert.ToByte(hexColor.Substring(6, 2), 16)));
 
                Color newColor = fillBrush.Color;
 
                double stopValue = i / 3.5;
                stopValue = stopValue / 100;
                var indexText = i.ToString();
 
                GradientStop newBrush = new GradientStop();
                newBrush.Color = fillBrush.Color;
                newBrush.Offset = stopValue;
 
                Grid container = new Grid();
                container.HorizontalAlignment = HorizontalAlignment.Stretch;
                container.VerticalAlignment = VerticalAlignment.Stretch;
                container.Margin = new Thickness(0);
                container.MinWidth = 175;
 
                Border background = new Border();
                background.HorizontalAlignment = HorizontalAlignment.Stretch;
                background.VerticalAlignment = VerticalAlignment.Stretch;
                background.Background = fillBrush;
 
                Ellipse listRectangle = new Ellipse();
                listRectangle.Height = 25;
                listRectangle.Width = 25;
                listRectangle.Fill = fillBrush;
 
                StackPanel colorPanel = new StackPanel();
                colorPanel.Orientation = Orientation.Horizontal;
                colorPanel.Children.Add(listRectangle);
                colorPanel.Children.Add(textBlock);
 
                container.Children.Add(background);
                container.Children.Add(colorPanel);
 
                ColorListTwo.Items.Add(container);
 
                previewList.Add(newColor);
                colorStops.Add(newBrush);
 
            }
 
            swatchRectangle.Fill = colorRangeBrush;
            HueSlider.Background = colorRangeBrush;
        }


To make sure that when it loads up the user can see the colors - I went ahead and have the values load up with some defaults.

public void setInitialPreviewColor(object sender, RoutedEventArgs e)
        {
 
            double lum = 50;
            double hue = 0;
            double sat = 100;
 
             var rgbColorTuple = rgbColorValues(hue, lum, sat);
             //var calculatedRGBValues = calcHSLRGBValues(hue, lum, sat);
 
             double colorRed = rgbColorTuple.Item1;
             double colorGreen = rgbColorTuple.Item2;
             double colorBlue = rgbColorTuple.Item3;
 
            RedValueDisplay.Text = colorRed.ToString();
            GreenValueDisplay.Text = colorGreen.ToString();
            BlueValueDisplay.Text = colorBlue.ToString();
 
            SolidColorBrush previewBrush = new SolidColorBrush(Color.FromArgb(255, (byte)rgbColorTuple.Item1, (byte)rgbColorTuple.Item2, (byte)rgbColorTuple.Item3));
            ColorPreview.Fill = previewBrush;
        }


You can download the Silverlight project here. 



No comments:

Post a Comment