Get color in specific location on gradient
Asked Answered
M

3

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?

Merat answered 10/3, 2012 at 20:46 Comment(1)
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.Glass
G
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);
Gunnery answered 10/3, 2012 at 23:21 Comment(6)
Johnny, do you think you could come over to #16162431 and post this answer? I'd like you to get the points.Offcenter
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.Inept
@Underdetermined: uh, oh, ...and where that edit is?Hook
@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.Inept
The second if statement should start with gs.Offset >= offset, otherwise boundary cases aren't handled correctly.Detent
The code was updated, and a disclaimer was add on the explanation to handle the problem.Gunnery
L
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}"/>
Liles answered 12/9, 2016 at 12:21 Comment(4)
I like the use of break to avoid needless iterationsAorist
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();Aorist
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.Liles
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.Aorist
A
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;
                }
            }
Accommodative answered 17/10, 2013 at 21:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.