#674 – Mapping Mouse Position to Color, part II

In the previous post, we used the mouse position to map to the Hue and Saturation portions of an HSV color value and then set the background color of a window based on the mouse position.

This post adds to the example, by using the mouse wheel to control the Value portion of the color (which ranges from 0.0 to 1.0).

Here’s the updated XAML:

<Window Name="win1" x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Color from Mouse Position"
        SizeToContent="WidthAndHeight"
        MouseMove="win1_MouseMove_1" MouseWheel="win1_MouseWheel_1">

    <Canvas x:Name="canv1" Width="400" Height="400">
        <Label x:Name="lblInfo" Content="{Binding RGBInfo}" HorizontalAlignment="Center" />
        <Ellipse x:Name="ellipseCenter" Stroke="Black" Width="2" Height="2" Fill="Black" Canvas.Left="199" Canvas.Top="199"/>
    </Canvas>
</Window>

And the updated code-behind:

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        SolidColorBrush backBrush = new SolidColorBrush();
        double value = 1.0;

        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = this;
            win1.Background = backBrush;
        }

        private string rgbInfo;
        public string RGBInfo
        {
            get { return rgbInfo; }
            set
            {
                if (value != rgbInfo)
                {
                    rgbInfo = value;
                    RaisePropertyChanged("RGBInfo");
                }
            }
        }

        private void win1_MouseMove_1(object sender, MouseEventArgs e)
        {
            RecalcColor(e.GetPosition(canv1));
        }

        private void win1_MouseWheel_1(object sender, MouseWheelEventArgs e)
        {
            if ((e.Delta > 0) && (value < 1.0))
                value += 0.1;
            else if ((e.Delta < 0) && (value > 0.0))
                value -= 0.1;

            RecalcColor(Mouse.GetPosition(canv1));
        }

        private void RecalcColor(Point mousePos)
        {
            double radius = (canv1.ActualWidth / 2);

            double hue;
            double saturation;
            Color c = ColorFromMousePosition(mousePos, radius, out hue, out saturation);
            backBrush.Color = c;
            RGBInfo = string.Format("R={0}, G={1}, B={2}.  H={3:F1}, S={4:F1}, V={5:F1}",
                c.R, c.G, c.B,
                hue, saturation, value);

            if (value < 0.5)
            {
                lblInfo.Foreground = Brushes.White;
                ellipseCenter.Stroke = Brushes.White;
            }
            else
            {
                lblInfo.Foreground = Brushes.Black;
                ellipseCenter.Stroke = Brushes.Black;
            }
        }

        private Color ColorFromMousePosition(Point mousePos, double radius, out double hue, out double saturation)
        {
            // Position relative to center of canvas
            double xRel = mousePos.X - radius;
            double yRel = mousePos.Y - radius;

            // Hue is angle in deg, 0-360
            double angleRadians = Math.Atan2(yRel, xRel);
            hue = angleRadians * (180 / Math.PI);
            if (hue < 0)
                hue = 360 + hue;

            // Saturation is distance from center
            saturation = Math.Min(Math.Sqrt(xRel * xRel + yRel * yRel) / radius, 1.0);

            byte r, g, b;
            ColorUtil.HsvToRgb(hue, saturation, value, out r, out g, out b);
            return Color.FromRgb(r, g, b);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
    }

(See the earlier post for the ColorUtil code).
We can now roll the mouse wheel down to reduce the value, making the colors darker.

#673 – Mapping Mouse Position to Color

You can map a 2D mouse position to a color by using the mouse position to map to the Hue and Saturation and then converting the HSV value to an RGB value.  (We just use 1.0 as the Value component of HSV).  We can map the mouse to a color wheel and use the angle of a line from the mouse position to the center of the wheel as the hue and use the distance from the center of the wheel as the saturation.

So we want to map a mouse position against a color wheel like the following:

In WPF, we’ll just create a Canvas, with a Label that will be used to dump out the RGB values.

<Window Name="win1" x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Color from Mouse Position"
        SizeToContent="WidthAndHeight"
        MouseMove="win1_MouseMove_1">

    <Canvas x:Name="canv1" Width="400" Height="400">
        <Label Content="{Binding RGBInfo}" HorizontalAlignment="Center" />
    </Canvas>
</Window>

In the code-behind, we’ll handle the MouseMove event for the window and re-calculate the color of the window’s background brush, based on the mouse position.

    /// <summary>
    ///
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        SolidColorBrush backBrush = new SolidColorBrush();

        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = this;
            win1.Background = backBrush;
        }

        private string rgbInfo;
        public string RGBInfo
        {
            get { return rgbInfo; }
            set
            {
                if (value != rgbInfo)
                {
                    rgbInfo = value;
                    RaisePropertyChanged("RGBInfo");
                }
            }
        }

        private void win1_MouseMove_1(object sender, MouseEventArgs e)
        {
            double radius = (canv1.ActualWidth / 2);

            Color c = ColorFromMousePosition(e.GetPosition(canv1), radius);
            backBrush.Color = c;
            RGBInfo = string.Format("R={0}, G={1}, B={2}", c.R, c.G, c.B);
        }

        Color ColorFromMousePosition(Point mousePos, double radius)
        {
            // Position relative to center of canvas
            double xRel = mousePos.X - radius;
            double yRel = mousePos.Y - radius;

            // Hue is angle in deg, 0-360
            double angleRadians = Math.Atan2(yRel, xRel);
            double hue = angleRadians * (180 / Math.PI);
            if (hue < 0)
                hue = 360 + hue;

            // Saturation is distance from center
            double saturation = Math.Min(Math.Sqrt(xRel * xRel + yRel * yRel) / radius, 1.0);

            byte r, g, b;
            ColorUtil.HsvToRgb(hue, saturation, 1.0, out r, out g, out b);
            return Color.FromRgb(r, g, b);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
    }

Notice that we make use of an HSV to RGB conversion function, found at http://www.splinter.com.au/converting-hsv-to-rgb-colour-using-c/. Below is the function as we’re using it.

    public static class ColorUtil
    {
        /// <summary>
        /// Convert HSV to RGB
        /// h is from 0-360
        /// s,v values are 0-1
        /// r,g,b values are 0-255
        /// Based upon http://ilab.usc.edu/wiki/index.php/HSV_And_H2SV_Color_Space#HSV_Transformation_C_.2F_C.2B.2B_Code_2
        /// </summary>
        public static void HsvToRgb(double h, double S, double V, out byte r, out byte g, out byte b)
        {
            // ######################################################################
            // T. Nathan Mundhenk
            // mundhenk@usc.edu
            // C/C++ Macro HSV to RGB

            double H = h;
            while (H < 0) { H += 360; };
            while (H >= 360) { H -= 360; };
            double R, G, B;
            if (V <= 0)
            { R = G = B = 0; }
            else if (S <= 0)
            {
                R = G = B = V;
            }
            else
            {
                double hf = H / 60.0;
                int i = (int)Math.Floor(hf);
                double f = hf - i;
                double pv = V * (1 - S);
                double qv = V * (1 - S * f);
                double tv = V * (1 - S * (1 - f));
                switch (i)
                {

                    // Red is the dominant color

                    case 0:
                        R = V;
                        G = tv;
                        B = pv;
                        break;

                    // Green is the dominant color

                    case 1:
                        R = qv;
                        G = V;
                        B = pv;
                        break;
                    case 2:
                        R = pv;
                        G = V;
                        B = tv;
                        break;

                    // Blue is the dominant color

                    case 3:
                        R = pv;
                        G = qv;
                        B = V;
                        break;
                    case 4:
                        R = tv;
                        G = pv;
                        B = V;
                        break;

                    // Red is the dominant color

                    case 5:
                        R = V;
                        G = pv;
                        B = qv;
                        break;

                    // Just in case we overshoot on our math by a little, we put these here. Since its a switch it won't slow us down at all to put these here.

                    case 6:
                        R = V;
                        G = tv;
                        B = pv;
                        break;
                    case -1:
                        R = V;
                        G = pv;
                        B = qv;
                        break;

                    // The color is not defined, we should throw an error.

                    default:
                        //LFATAL("i Value error in Pixel conversion, Value is %d", i);
                        R = G = B = V; // Just pretend its black/white
                        break;
                }
            }
            r = Clamp((byte)(R * 255.0));
            g = Clamp((byte)(G * 255.0));
            b = Clamp((byte)(B * 255.0));
        }

        /// <summary>
        /// Clamp a value to 0-255
        /// </summary>
        private static byte Clamp(byte i)
        {
            if (i < 0) return 0;
            if (i > 255) return 255;
            return i;
        }
    }

The final result is a window whose background color changes as we move the mouse around.

#222 – Example: Changing a Color Using RGB Sliders

Here’s an example of changing a color at run-time, using three sliders.

The XAML defines a control and three sliders:

	<Window.Resources>
		<SolidColorBrush x:Key="magicBrush" Color="Red"/>
	</Window.Resources>

	<StackPanel>
		<Ellipse Height="100" Width="200" Fill="{StaticResource magicBrush}" Margin="30"/>

		<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
			<Label Content="Red" VerticalAlignment="Center"/>
        	<Slider Name="sliR" Width="150" Margin="10" Minimum="0" Maximum="255" TickFrequency="1" IsSnapToTickEnabled="True"
				Value="255"
				ValueChanged="sli_ValueChanged"/>
			<Label Content="{Binding ElementName=sliR, Path=Value}" VerticalAlignment="Center"/>
		</StackPanel>
		<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
			<Label Content="Green" VerticalAlignment="Center"/>
        	<Slider Name="sliG" Width="150" Margin="10" Minimum="0" Maximum="255" TickFrequency="1" IsSnapToTickEnabled="True"
				ValueChanged="sli_ValueChanged"/>
			<Label Content="{Binding ElementName=sliG, Path=Value}" VerticalAlignment="Center"/>
		</StackPanel>
		<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
			<Label Content="Blue" VerticalAlignment="Center"/>
        	<Slider Name="sliB" Width="150" Margin="10" Minimum="0" Maximum="255" TickFrequency="1" IsSnapToTickEnabled="True"
				ValueChanged="sli_ValueChanged"/>
			<Label Content="{Binding ElementName=sliB, Path=Value}" VerticalAlignment="Center"/>
		</StackPanel>
	</StackPanel>

In code, we have a single method that executes when the sliders change, which changes the brush color:

        private void sli_ValueChanged(object sender, System.Windows.RoutedPropertyChangedEventArgs<double> e)
        {
			SolidColorBrush magicBrush = (SolidColorBrush)this.Resources["magicBrush"];

            if ((sliR != null) && (sliG != null) && (sliB != null))
            {
                magicBrush.Color = Color.FromRgb((byte)sliR.Value, (byte)sliG.Value, (byte)sliB.Value);
            }
        }

#211 – Creating a Color Value in Code

Color, defined in System.Windows.Media, is a structure that represents a color by storing Red (R), Green (G) and Blue (B) values as well as an Alpha (A) value indicating opacity.

You can create a color value in code using the Color.FromRgb and Color.FromArgb static methods.

The Color.FromRgb method takes three parameters, allowing you specify the R, G and B components of the color, each of which is in the range 0-255.

            Color lightGreen = Color.FromRgb(120, 255, 0);
            this.Background = new SolidColorBrush(lightGreen);

The Color.FromArgb method also takes a parameter representing the alpha channel (0-255), which dictates the opacity of the color.  A value of 255 represents a fully opaque color and a value of 0 represents a fully transparent color.

            // Set window background to semi-transparent color
            Color lightGreen = Color.FromArgb(50, 120, 255, 0);
            this.Background = new SolidColorBrush(lightGreen);

#210 – Specifying Colors in XAML As RGB Values

The type converter that converters names of colors to a SolidColorBrush of the specified color will also create a brush of a specified RGB color.  You can specify RGB color values in XAML in one of several ways.

In the example below, we specify each of the R, G and B values as a 2 hex-digit number (00-FF), representing the range 0-255.  The digits are ordered RRGGBB, so the value #FF0000 is solid red.

        <Button Height="25" Width="100" Content="A button" Background="#FF0000"/>

Here are some other formats that you can use:

  • #rgb – RGB value with only 1 digit per color (0-F).  E.g. #FF0 (Red+Green = Yellow)
  • #argb – single-digit RGB + alpha value (0-F)
  • #aarrggbb – 2 digits for R, G, B + 2 digits for alpha channel (transparency)
  • sc#scA,scR,scG,scB – scRGB formatted number, each value ranging from 0.0 to 1.0

#209 – Specifying a Color Where a Brush Is Expected in XAML

We saw earlier how to set the Background property for a control in XAML, by setting the property to an instance of a SolidColorBrush and then setting the Color property for the brush:

    <Window.Background>
        <SolidColorBrush Color="PowderBlue"/>
    </Window.Background>

We can be much more concise than this, however, just setting the value of the Background property directly to a color:

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="346" Width="590"
        Background="PowderBlue">

This works through the use of type converters.  The Background property uses a type converter to convert a string to an instance of a SolidColorBrush of the corresponding color.  Specifically, it sets the property to point to the preexisting Brushes.PowderBlue brush.  You can see the full list of predefined brushes here.

#208 – Color Values Are Stored as RGB Values

Although we can specify colors for brushes in WPF using predefined names, e.g. “DarkBlue”, each color is actually stored as an RGB (Red/Green/Blue) value.

For example, if we specify a color as “Lavender” and then look at the Color property of the SolidColorBrush at runtime, we see that the color is actually a structure containing a number of fields.

The main fields in this structure are the R, G, and B fields, which specify Red, Green and Blue values that make up the resulting color.  Each value has a range from 0-255.  So “Lavender” is: R=230, G=230, B=250.

The A field represents the alpha channel (also 0-255), which dictates that transparency of the object (0 = fully transparent, 255 = fully opaque).

The ScA, ScR, ScG, and ScB properties allowing reading or writing the color in the scRGB format.