14

I have the following GradientStopCollection:

GradientStopCollection grsc = new GradientStopCollection(3);
grsc.Add(new GradientStop(Colors.Red, 0));
grsc.Add(new GradientStop(Colors.Yellow, .5));
grsc.Add(new GradientStop(Colors.Green, 1));

Can I get the color at a specific "location"? For example:

  • Location 0: Red
  • Location .5: Yellow
  • Location .75: Yellow<~>Green

Is there an API in WPF / some third party library that could do that?

Matan Shahar
  • 3,190
  • 2
  • 20
  • 45
  • I don't think this is defined anywhere in WPF. I would expect it to depend on your video card driver's implementation, the zoom level, the users color depth, etc. You can use Visual.PointToScreen method and then Graphics.CopyFromScreen to grab that pixel. Then use Bitmap.GetPixel to retrieve the color details. – akhisp Mar 10 '12 at 21:19

3 Answers3

19

To get a color at a specific point is necessary to understand the gradient in question, and this is not the role of class GradientStopCollection. The concept of this class is not to understand a gradient, but should be a simple collection of support to a gradient.

Is important that you understand the concept of each class.

To get a color, you need to instantiate a class that represents a gradient using the gradient to paint and finally get their color from the painting.

but I'll give you a quicker solution. You can use a gradient algorithm to generate a single point. This is an implementation of how to do this using a linear gradient algorithm:

public static class GradientStopCollectionExtensions
{
    public static Color GetRelativeColor(this GradientStopCollection gsc, double offset)
    {
        var point = gsc.SingleOrDefault(f => f.Offset == offset);
        if (point != null) return point.Color;

        GradientStop before = gsc.Where(w => w.Offset == gsc.Min(m => m.Offset)).First();
        GradientStop after = gsc.Where(w => w.Offset == gsc.Max(m => m.Offset)).First();

        foreach (var gs in gsc)
        {
            if (gs.Offset < offset && gs.Offset > before.Offset)
            {
                before = gs;
            }
            if (gs.Offset > offset && gs.Offset < after.Offset)
            {
                after = gs;
            }
        }

        var color = new Color();

        color.ScA = (float)((offset - before.Offset) * (after.Color.ScA - before.Color.ScA) / (after.Offset - before.Offset) + before.Color.ScA);
        color.ScR = (float)((offset - before.Offset) * (after.Color.ScR - before.Color.ScR) / (after.Offset - before.Offset) + before.Color.ScR);
        color.ScG = (float)((offset - before.Offset) * (after.Color.ScG - before.Color.ScG) / (after.Offset - before.Offset) + before.Color.ScG);
        color.ScB = (float)((offset - before.Offset) * (after.Color.ScB - before.Color.ScB) / (after.Offset - before.Offset) + before.Color.ScB);

        return color;
    }
}

PS: This algorithm assumes there are no stops with the same offset. If there are multiple stops with the same offset a InvalidOperationException will be thrown.

Add this class in your current context (namespace context)

To get your color in any place you insert something like this:

var color = grsc.GetRelativeColor(.75);
Jonny Piazzi
  • 3,684
  • 4
  • 34
  • 81
  • 1
    Johnny, do you think you could come over to http://stackoverflow.com/questions/16161931/how-to-read-the-color-from-an-offset-of-a-xaml-lineargradientbrush and post this answer? I'd like you to get the points. – Rob Perkins Apr 23 '13 at 22:49
  • This is exactly what I was looking for, with one flaw that if the offset is exactly equal to a gradientstop it will ignore that gradient stop entirely. Hence my edit. – Underdetermined Jun 03 '16 at 16:34
  • @Underdetermined: uh, oh, ...and where that edit is? – quetzalcoatl Feb 25 '18 at 15:38
  • @quetzalcoatl: unfortunatley my edit seemed not to have passed peer review and/or got lost. Its been 1.5 years and I dont remember exactly what I changed (and can't look for my source code as i was working a different company back then. Since this was the only time I worked in c# i'll leave someone more compitent to make the edit, for this code to work. – Underdetermined Mar 04 '18 at 09:13
  • The second if statement should start with gs.Offset >= offset, otherwise boundary cases aren't handled correctly. – Lokno Sep 06 '18 at 15:16
  • The code was updated, and a disclaimer was add on the explanation to handle the problem. – Jonny Piazzi Sep 10 '18 at 16:22
4

I've tried the method written by Jonny Piazzi. But it didn't work correctly.
So I write my own one below:

private static Color GetColorByOffset(GradientStopCollection collection, double offset)
{
    GradientStop[] stops = collection.OrderBy(x => x.Offset).ToArray();
    if (offset <= 0) return stops[0].Color;
    if (offset >= 1) return stops[stops.Length - 1].Color;
    GradientStop left = stops[0], right = null;
    foreach (GradientStop stop in stops)
    {
        if (stop.Offset >= offset)
        {
            right = stop;
            break;
        }
        left = stop;
    }
    Debug.Assert(right != null);
    offset = Math.Round((offset - left.Offset)/(right.Offset - left.Offset), 2);
    byte a = (byte) ((right.Color.A - left.Color.A)*offset + left.Color.A);
    byte r = (byte) ((right.Color.R - left.Color.R)*offset + left.Color.R);
    byte g = (byte) ((right.Color.G - left.Color.G)*offset + left.Color.G);
    byte b = (byte) ((right.Color.B - left.Color.B)*offset + left.Color.B);
    return Color.FromArgb(a, r, g, b);
}

I hope it works for you!

I've used this method in my xaml code below to show a specified number as heat map position.

<LinearGradientBrush x:Key="CountBrush" StartPoint="0 0" EndPoint="1 0">
    <GradientStop Offset="0.00" Color="ForestGreen"/>
    <GradientStop Offset="0.50" Color="Yellow"/>
    <GradientStop Offset="1.00" Color="OrangeRed"/>
</LinearGradientBrush>
<local:Int32ToColorConverter x:Key="CountToColorConverter" Min="0" Max="200" LinearBrush="{StaticResource CountBrush}"/>
walterlv
  • 2,366
  • 13
  • 73
  • I like the use of break to avoid needless iterations – Wobbles Jan 03 '17 at 11:42
  • You can simplify the code by removing the visible iterations and using somthing like `GradientStop left = stops.Where(s => s.Offset <= offset).Last(); GradientStop right = stops.Where(s => s.Offset > offset).First();` – Wobbles Jan 14 '17 at 20:34
  • Thanks! It really simplified my code. In this case, all of my "`{`" and "`}`" are gone. But I guess you should use `FirstOrDefault` and `LastOrDefault` with `??` instead of `First` and `Last`. – walterlv Jan 19 '17 at 07:47
  • In my usage I used a separate conditional(s) to check if the offset is before the first stop or after the last to avoid even the iterators in the `.Where()`. so it would break out before `.First()` or `.Last()` had a chance to return null. – Wobbles Jan 19 '17 at 13:15
0
  foreach (var gs in gsc)
            {
                if (gs.Offset == offset) return gs.Color; //new line added
                if (gs.Offset < offset && gs.Offset > before.Offset)
                {
                    before = gs;
                }

                if (gs.Offset > offset && gs.Offset < after.Offset)
                {
                    after = gs;
                }
            }
Mixer
  • 1,292
  • 3
  • 22
  • 41