Theodore Zographos
Theodore Zographos

Reputation: 2395

WPF Color interpolation

I am trying to paint a WPF control's background based on a palette where each color has been assigned with values (e.g. Red = 0, DarkGreen = 10, Green = 20,LightGreen =30) and a user-selected value (e.g. 25) that would give the resulting color. I would like the resulting color to be an interpolation between the 2 nearest color values (e.g. for a value of 25 it should give a color between Green and LightGreen)

For that I'm thinking of using the existing LinearGradientBrush in WPF; set the GradientStops, offsets and get the color at a specified value . Is there a way to do this or should I implement my own color interpolation function?

Thanks.

Upvotes: 2

Views: 4282

Answers (6)

Peter Huber
Peter Huber

Reputation: 3312

The proper way is if you work with hue, saturation and brilliance of a color. You can convert and RGB color to their HSB values using a method like this:

public static (int Hue, double Saturation, double Brightness)GetHSB(this Color color) {
  int max = Math.Max(color.R, Math.Max(color.G, color.B));
  int min = Math.Min(color.R, Math.Min(color.G, color.B));
  int hue = 0;//for black, gray or white, hue could be actually any number, but usually 0 is 
              //assign, which means red
  if (max-min!=0) {
    //not black, gray or white
    int maxMinDif = max-min;
    if (max==color.R) {
      #pragma warning disable IDE0045 // Convert to conditional expression
      if (color.G>=color.B) {
      #pragma warning restore IDE0045
        hue = 60 * (color.G-color.B)/maxMinDif;
      } else {
        hue = 60 * (color.G-color.B)/maxMinDif + 360;
      }
    } else if (max==color.G) {
      hue = 60 * (color.B-color.R)/maxMinDif + 120;
    } else if(max == color.B) {
      hue = 60 * (color.R-color.G)/maxMinDif + 240;
    }
  }

  double saturation = (max == 0) ? 0.0 : (1.0-((double)min/(double)max));

  return (hue, saturation, (double)max/0xFF);
}

The hue defines the color, like red or blue from 0 to 360 degrees. If you want a gradient between blue (240 degrees) and red (360 degrees), you divide the 120 degrees by the number of steps and convert it back to RGB. But this is a bit complicated. Easier is just to mix the 2 colors using different weights:

/// <summary>
/// Mixes factor*color1 with (1-factor)*color2.
/// </summary>
public static Color Mix(this Color color1, double factor, Color color2) {
  if (factor<0) throw new Exception($"Factor {factor} must be greater equal 0.");
  if (factor>1) throw new Exception($"Factor {factor} must be smaller equal 1.");

  if (factor==0) return color2;
  if (factor==1) return color1;

  var factor1 = 1 - factor;
  return Color.FromArgb(
    (byte)((color1.A * factor + color2.A * factor1)),
    (byte)((color1.R * factor + color2.R * factor1)),
    (byte)((color1.G * factor + color2.G * factor1)),
    (byte)((color1.B * factor + color2.B * factor1)));
}

I wrote a long article on CodeProject about precisely this topic: Definitive Guide to WPF Colors, Color Spaces, Color Pickers and Creating Your Own Colors for Mere Mortals

Upvotes: 0

Jas Laferriere
Jas Laferriere

Reputation: 824

I'm not sure if this was the case back then but in .NET 4.0 getting a color from a LinearGradientBrush can be done.

private Color GetColor(double ratio)
{
    if (ratio < 0) ratio = 0;
    else if (ratio > 1) ratio = 1;

    //Find gradient stops that surround the input value
    GradientStop gs0 = ColorScale.GradientStops.Where(n => n.Offset <= ratio).OrderBy(n => n.Offset).Last();
    GradientStop gs1 = ColorScale.GradientStops.Where(n => n.Offset >= ratio).OrderBy(n => n.Offset).First();

    float y = 0f;
    if (gs0.Offset != gs1.Offset)
    {
        y = (float)((ratio - gs0.Offset) / (gs1.Offset - gs0.Offset));
    }

    //Interpolate color channels
    Color cx = new Color();
    if (ColorScale.ColorInterpolationMode == ColorInterpolationMode.ScRgbLinearInterpolation)
    {
        float aVal = (gs1.Color.ScA - gs0.Color.ScA) * y + gs0.Color.ScA;
        float rVal = (gs1.Color.ScR - gs0.Color.ScR) * y + gs0.Color.ScR;
        float gVal = (gs1.Color.ScG - gs0.Color.ScG) * y + gs0.Color.ScG;
        float bVal = (gs1.Color.ScB - gs0.Color.ScB) * y + gs0.Color.ScB;
        cx = Color.FromScRgb(aVal, rVal, gVal, bVal);
    }
    else
    {
        byte aVal = (byte)((gs1.Color.A - gs0.Color.A) * y + gs0.Color.A);
        byte rVal = (byte)((gs1.Color.R - gs0.Color.R) * y + gs0.Color.R);
        byte gVal = (byte)((gs1.Color.G - gs0.Color.G) * y + gs0.Color.G);
        byte bVal = (byte)((gs1.Color.B - gs0.Color.B) * y + gs0.Color.B);
        cx = Color.FromArgb(aVal, rVal, gVal, bVal);
    }
    return cx;
}

This would work with a brush configured as follows (for example):

var brush = new LinearGradientBrush();
brush.StartPoint = new Point(0, 0);
brush.EndPoint = new Point(1, 0);

//Set brush colors
brush.GradientStops.Add(new GradientStop() { Color = Color.FromRgb(102, 40, 0), Offset = 0 });
brush.GradientStops.Add(new GradientStop() { Color = Color.FromRgb(254, 167, 80), Offset = 0.25 });
brush.GradientStops.Add(new GradientStop() { Color = Color.FromRgb(0, 153, 51), Offset = 0.5 });
brush.GradientStops.Add(new GradientStop() { Color = Color.FromRgb(232, 165, 255), Offset = 0.75 });
brush.GradientStops.Add(new GradientStop() { Color = Color.FromRgb(66, 0, 89), Offset = 1 });

Source: http://dotupdate.wordpress.com/2008/01/28/find-the-color-of-a-point-in-a-lineargradientbrush/

Upvotes: 2

Theodore Zographos
Theodore Zographos

Reputation: 2395

Thanks for all the replies guys. It seems there is no way to get the "value" of a GradientBrush at a specified point. I hope this is corrected at some later version of the framework. So I guess the only option for now is to implement an interpolation algorithm as Mikko suggested.

Upvotes: 1

Nir
Nir

Reputation: 29594

I think you'll be better of using a value converter, just take one of the interpolation functions suggested in the other answers and put in in a value converter, use this converter to bind the background property to the value and you're done.

Upvotes: 1

Mikko Rantanen
Mikko Rantanen

Reputation: 8084

Using the LinearGradientBrush sounds like it would have a bit of an overhead. No knowledge though. A color interpolation function isn't that hard to write though.

I'm assuming your palettes have values that are divisible by 10 for simplicity.

public static Color GetColor(int value)
{
    int startIndex = (value/10)*10;
    int endIndex = startIndex + 10;

    Color startColor = Palette[startIndex];
    Color endColor = Palette[endIndex];

    float weight = (value - startIndex)/(float)(endIndex - startIndex);

    return Color.FromArgb(
        (int)Math.Round(startColor.R * (1 - weight) + endColor.R * weight),
        (int)Math.Round(startColor.G * (1 - weight) + endColor.G * weight),
        (int)Math.Round(startColor.B * (1 - weight) + endColor.B * weight));

}

If the defined colors are not divisible by 10 the logic to find the start and end colors will be a bit more complex.

Upvotes: 5

Eoin Campbell
Eoin Campbell

Reputation: 44268

Where did you come up with the Values 10/20/30 for your DarkGreen/Green/Lightgreen colors.

You'll need some sort of correlation table between your assigned palette values & the real numeric representations of the colors... e.g.

Color             Pal-Code     RGB            HSL
Red               0            255,0,0        0,240,120
Dark Green        10           0,128,0        80,240,60
Green             20           0,255,0        80,240,120
Light Green       30           128,255,128    80,240,180

From that correlation table, you could take any user "palette code", find the closed matching pair of palette codes from the table above and do a best-match range find on it. e.g. if some entered 25 (let's use HSL for convenience) then the formula would be...

Green             20           0,255,0        80,240,120
Light Green       30           128,255,128    80,240,180

25 is halfway between both codes so

Palette Code     Hue        Sat      Luminence
20               80         240      120
30               80         240      180
-------------------------------------------------
25               80         240      150 

If they had selected 6, you'd need to find .6 of the range of colors between each value.

Red               0            255,0,0        0,240,120
Dark Green        10           0,128,0        80,240,60

Palette Code     Hue        Sat      Luminence
0                0          240      120
10               80         240      60
-------------------------------------------------
6                48         240      84

0->80      = +80 * 60%   = +48    So 0+48   = 48
240->240   =   0 * 60%   = 0      So 240+0  = 240
120->60    = -60 * 60%   = -36    So 120-36 = 84

Upvotes: 1

Related Questions