Map numeric values to and from colors in a color gradient in C#

Map numeric values to and from colors

Sometimes it’s useful to map numeric values to and from colors. For example, intensity of color could indicate population density, agricultural yield, rainfall, or other values on a map.

This program draws a color-selection rainbow that includes the colors red, yellow, green, aqua, blue, fuchsia, and red again. (You can draw a rainbow using just red, green, blue, and red again but it’s not as bright and the intermediate colors yellow, aqua, and fuchsia don’t show up well.)

The static Rainbow class provides methods for creating a brush that uses those colors, and methods to convert a color to and from a numeric position along the gradient where 0 represents red at the start of the gradient, 1/6 represents yellow, 2/6 represents green, and so forth.

The RainbowBrush method shown in the following code returns a rainbow brush starting at point1 and ending at point2.

// Return a rainbow brush.
// The calling code should dispose of the brush.
public static LinearGradientBrush RainbowBrush(
    Point point1, Point point2)
{
    LinearGradientBrush rainbow_brush =
        new LinearGradientBrush(point1, point2,
            Color.Red, Color.Red);

    // Define the colors along the gradient.
    ColorBlend color_blend = new ColorBlend();
    color_blend.Colors = new Color[]
        { Color.Red, Color.Yellow, Color.Lime, Color.Aqua,
          Color.Blue, Color.Fuchsia, Color.Red };
    color_blend.Positions = new float[]
        { 0, 1 / 6f, 2 / 6f, 3 / 6f, 4 / 6f, 5 / 6f, 1 };
    rainbow_brush.InterpolationColors = color_blend;

    return rainbow_brush;
}

The method starts by creating a LinearGradientBrush. It then creates a ColorBlend object to define the colors and their positions in the gradient. It sets the brush’s InterpolationColors property to the ColorBlend object and returns the brush.

Note that the calling code should dispose of the brush after using it, possibly with a using statement.

The ColorToRainbowNumber method shown in the following code maps a color to a location along the gradient.

// Map a color to a rainbow number between 0 and 1 on the
// Red-Yellow-Green-Aqua-Blue-Fuchsia-Red rainbow.
public static float ColorToRainbowNumber(Color clr)
{
    // See which color is weakest.
    int r = clr.R;
    int g = clr.G;
    int b = clr.B;
    if ((r <= g) && (r <= b))
    {
        // Red is weakest. It's mostly blue and green.
        g -= r;
        b -= r;
        if (g + b == 0) return 0;
        return (2 / 6f * g + 4 / 6f * b) / (g + b);
    }
    else if ((g <= r) && (g <= b))
    {
        // Green is weakest. It's mostly red and blue.
        r -= g;
        b -= g;
        if (r + b == 0) return 0;
        return (1f * r + 4 / 6f * b) / (r + b);
    }
    else
    {
        // Blue is weakest. It's mostly red and green.
        r -= b;
        g -= b;
        if (r + g == 0) return 0;
        return (0f * r + 2 / 6f * g) / (r + g);
    }
}

One key idea is that the code ignores the smallest color component value. For example, if the color’s red, green, and blue components are 255, 255, and 128, then this color is mostly red and green with less blue, so the code ignores the blue component.

This is necessary because the gradient only contains saturated colors. These colors have at most 2 non-zero color components. Other non-saturated colors are hues of the basic colors. They are basically the saturated colors with extra white or gray added.

This method starts by deciding which color component to ignore. It then finds the color’s location by using a weighted average of the remaining color components’ positions in the gradient. For example, consider the color (30, 60, 120). The red component is smallest so the code ignores it. The green and blue components are 60 and 120. Green is located at position 2/6 on the gradient and blue is at 4/6 so the location for this color is given by the weighted average:



If the green component is small and the blue component is large, the result is close to blue. If the two are close to equal, the result is about halfway between green and blue.

The RainbowNumberToColor method shown in the following code performs the inverse, mapping a number between 0 and 1 to a color on the gradient.

// Map a rainbow number between 0 and 1 to a color on the
// Red-Yellow-Green-Aqua-Blue-Fuchsia-Red rainbow.
public static Color RainbowNumberToColor(float number)
{
    byte r = 0, g = 0, b = 0;
    if (number < 1 / 6f)
    {
        // Mostly red with some green.
        r = 255;
        g = (byte)(r * (number - 0) / (2 / 6f - number));
    }
    else if (number < 2 / 6f)
    {
        // Mostly green with some red.
        g = 255;
        r = (byte)(g * (2 / 6f - number) / (number - 0));
    }
    else if (number < 3 / 6f)
    {
        // Mostly green with some blue.
        g = 255;
        b = (byte)(g * (2 / 6f - number) / (number - 4 / 6f));
    }
    else if (number < 4 / 6f)
    {
        // Mostly blue with some green.
        b = 255;
        g = (byte)(b * (number - 4 / 6f) / (2 / 6f - number));
    }
    else if (number < 5 / 6f)
    {
        // Mostly blue with some red.
        b = 255;
        r = (byte)(b * (4 / 6f - number) / (number - 1f));
    }
    else
    {
        // Mostly red with some blue.
        r = 255;
        b = (byte)(r * (number - 1f) / (4 / 6f - number));
    }
    return Color.FromArgb(r, g, b);
}

This code is fairly complicated but the idea is simple. The code examines the number to see in which sixth of the gradient it lies. It then sets the color component closest to that section of the gradient to 255. For example, if the color is close to green, the code sets the green component to 255.

Next the code uses the weighted average formulas shown previously to find the other color component’s value. The new formulas are simply the previous formulas solved for the appropriate color component values. For example, if the color is close to green between green and blue, the code sets the red component to 0, green to 255, and solves the weighted average equation to find the corresponding blue component.

When you move the mouse over its upper PictureBox, the program uses the following code to demonstrate these methods.

// The currently selected color and its number.
private Color SelectedColor;
private float SelectedRainbowNumber;

// Select this color.
private void picRainbow_MouseMove(object sender, MouseEventArgs e)
{
    MouseMoving = true;

    // Get the mouse position as a fraction
    // of the width of the PictureBox.
    float rainbow_color = e.X / (float)picRainbow.ClientSize.Width;

    // Convert into the corresponding color.
    SelectedColor = Rainbow.RainbowNumberToColor(rainbow_color);

    // Convert back into the corresponding number.
    SelectedRainbowNumber =
        Rainbow.ColorToRainbowNumber(SelectedColor);
    txtValue.Text = SelectedRainbowNumber.ToString("0.00");

    // Redraw.
    picRainbow.Refresh();
    picSample.Refresh();

    MouseMoving = false;
}

The code calculates the mouse’s X position as a fraction of the PictureBox‘s width and uses the RainbowNumberToColor method to convert that into a color on the gradient.

Next the code uses the ColorToRainbowNumber method to convert the color back into a number. It displays the number in the txtValue text box.

The method finishes by refreshing the two PictureBoxes to display the color and the number. The following code shows how the PictureBoxes display those values.

// Draw the rainbow and the selected number.
private void picRainbow_Paint(object sender, PaintEventArgs e)
{
    // Draw the rainbow.
    using (Brush rainbow_brush = Rainbow.RainbowBrush(
        new Point(picRainbow.Left, picRainbow.Top),
        new Point(picRainbow.Right, picRainbow.Top)))
    {
        e.Graphics.FillRectangle(rainbow_brush,
            picRainbow.ClientRectangle);
    }

    // Get and draw the selected location.
    int x = (int)(SelectedRainbowNumber *
            picRainbow.ClientSize.Width);
    Point[] pts =
    {
        new Point(x - 5, 0),
        new Point(x, 5),
        new Point(x + 5, 0)
    };
    e.Graphics.FillPolygon(Brushes.Black, pts);
}

// Draw the sample color.
private void picSample_Paint(object sender, PaintEventArgs e)
{
    picSample.BackColor = SelectedColor;
}

The picRainbow control’s Paint event handler fills the control with the gradient brush. It then draws a black triangle over the position given by SelectRainbowNumber, scaled across the width of the control.

The picSample control’s Paint event handler simply fills the control with SelectedColor so you can see what color was selected.

The fact that the color in the gradient under the mouse matches the sample color at the bottom of the program, and the fact that the black triangle at the top of the gradient is above the mouse shows that the two methods work.

Finally, the following event handler executes when you change the text in the text box.

// True if we are updating the color already.
private bool MouseMoving = false;
private void txtValue_TextChanged(object sender, EventArgs e)
{
    if (MouseMoving) return;

    // Try to get the value as a fraction between 0 and 1.
    try
    {
        // Get the value from the text box.
        SelectedRainbowNumber = float.Parse(txtValue.Text);

        // Convert into the corresponding color.
        SelectedColor = Rainbow.RainbowNumberToColor(
            SelectedRainbowNumber);

        // Redraw.
        picRainbow.Refresh();
        picSample.Refresh();
    }
    catch
    {
    }
}

First the code verifies that the text box wasn’t changed by the MouseMove event handler. If it wasn’t, the code parses the text you entered and saves the result in the variable SelectedRainbowColor. It then uses RainbowNumberToColor to get the corresponding color, and saves it in the SelectedColor variable. It then refreshes to two PictureBoxes so you can see the selected color.


Download Example   Follow me on Twitter   RSS feed




This entry was posted in algorithms, graphics, mathematics and tagged , , , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *