Search code examples
c#wpfxamldirectxshader

Angle Gradient With Multiple GradientStops for XAML


I'm trying to create an angle gradient (similar to the one Photoshop does) in XAML. I found this article: https://stackoverflow.com/a/21096028/2555957 which does work but only supports two gradient stops and I need 11.

I've looked over the shader that does the calculations in the backend but I don't know how to add support for more gradient stops. It would work even if the shader would be hardcoded to work with 11 colors too.


Solution

  • Here's a shader that is similar to this one that supports twenty stops and has no aliasing around the edges:

    enter image description here

    Setup:

    • Put the shader code in shader.hlsl and compile it using fxc.exe. The command that I ran was "fxc /T ps_3_0 /Fo shader.ps shader.hlsl".
    • Add the output file shader.ps to the root of your project and change its build type to resource.
    • Add the effect code and xaml to your project.

    Xaml:

    <Canvas Height="405"
            Margin="50">
        <Ellipse Width="400"
                 Height="400"
                 Fill="Transparent"
                 Stroke="Red"
                 StrokeThickness="15">
            <Ellipse.Effect>
                <local:AngularGradientEffect>
                    <local:AngularGradientEffect.GradientStops>
                        <GradientStop Offset="0.0"
                                          Color="#41b1e1"></GradientStop>
                        <GradientStop Offset=".1"
                                          Color="#3e3e3e"></GradientStop>
                        <GradientStop Offset=".2"
                                          Color="#41b1e1"></GradientStop>
                        <GradientStop Offset=".3"
                                          Color="#3e3e3e"></GradientStop>
                        <GradientStop Offset=".4"
                                          Color="#41b1e1"></GradientStop>
                        <GradientStop Offset=".5"
                                          Color="#3e3e3e"></GradientStop>
                        <GradientStop Offset=".6"
                                          Color="#41b1e1"></GradientStop>
                        <GradientStop Offset=".7"
                                          Color="#3e3e3e"></GradientStop>
                        <GradientStop Offset=".8"
                                          Color="#41b1e1"></GradientStop>
                        <GradientStop Offset=".9"
                                          Color="#3e3e3e"></GradientStop>
                        <GradientStop Offset="1"
                                          Color="#41b1e1"></GradientStop>
                    </local:AngularGradientEffect.GradientStops>
                </local:AngularGradientEffect>
            </Ellipse.Effect>
        </Ellipse>
    </Canvas>
    

    I reused LinearGradientBrush's GradientStopCollection/GradientStop for consistency.

    AngularGradientEffect.cs:

    public class AngularGradientEffect : ShaderEffect
    {
        const int STOP_COUNT = 20;
        const int STOP_ANGLE_OFFSET = 10;
        const int STOP_COLOR_OFFSET = 50;
    
        public static readonly DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty(
            "Input",
            typeof(AngularGradientEffect),
            0);
    
        public static readonly DependencyProperty CenterPointProperty = DependencyProperty.Register(
            "CenterPoint",
            typeof(Point),
            typeof(AngularGradientEffect),
            new UIPropertyMetadata(new Point(0.5D, 0.5D), PixelShaderConstantCallback(0)));
    
        public static readonly DependencyProperty GradientStopsProperty =
            DependencyProperty.Register("GradientStops",
            typeof(GradientStopCollection),
            typeof(AngularGradientEffect),
            new PropertyMetadata(new GradientStopCollection()));
    
        static readonly DependencyProperty[] StopAngles = new DependencyProperty[STOP_COUNT];
        static readonly DependencyProperty[] StopColors = new DependencyProperty[STOP_COUNT];
    
        static AngularGradientEffect()
        {
            for (int i = 0; i < STOP_COUNT; i++)
            {
                StopAngles[i] = DependencyProperty.Register("GradientStopAngle" + i, typeof(float), typeof(AngularGradientEffect), new UIPropertyMetadata(-1.0f, PixelShaderConstantCallback(STOP_ANGLE_OFFSET + i)));
                StopColors[i] = DependencyProperty.Register("GradientStopColor" + i, typeof(Color), typeof(AngularGradientEffect), new UIPropertyMetadata(Colors.RosyBrown, PixelShaderConstantCallback(STOP_COLOR_OFFSET + i)));
            }
        }
    
        public AngularGradientEffect()
        {
            PixelShader = new PixelShader()
            {
                UriSource = new Uri("/ShaderTest;component/shader.ps", UriKind.Relative)
            };
    
            UpdateShaderValue(InputProperty);
            UpdateShaderValue(CenterPointProperty);
            GradientStops = new GradientStopCollection();
            GradientStops.Changed += GradientStops_Changed;
        }
    
        void GradientStops_Changed(object sender, EventArgs e)
        {
            SetGradientStopDependencyProperties(sender as GradientStopCollection);
        }
    
        public void SetGradientStopDependencyProperties(IEnumerable<GradientStop> stops)
        {
            var orderedStops = stops.OrderBy(s => s.Offset).ToArray();
    
            for (int i = 0; i < STOP_COUNT; i++)
            {
                var current = orderedStops.ElementAtOrDefault(i);
    
                DependencyProperty angleProperty = StopAngles[i];
                SetValue(angleProperty, current == null ? -1.0f : (float)current.Offset * 2 * 3.141596f);
    
                DependencyProperty colorProperty = StopColors[i];
                SetValue(colorProperty, current == null ? Color.FromArgb(0, 0, 0, 0) : current.Color);
            }
        }
    
        public Brush Input
        {
            get
            {
                return ((Brush)(this.GetValue(InputProperty)));
            }
            set
            {
                this.SetValue(InputProperty, value);
            }
        }
    
        public Point CenterPoint
        {
            get
            {
                return ((Point)(this.GetValue(CenterPointProperty)));
            }
            set
            {
                this.SetValue(CenterPointProperty, value);
            }
        }
    
        public GradientStopCollection GradientStops
        {
            get { return (GradientStopCollection)GetValue(GradientStopsProperty); }
            set
            {
                SetValue(GradientStopsProperty, value);
            }
        }
    }
    

    Shader:

    sampler2D  inputSampler : register(S0);
    
    float2 centerPoint : register(C0);
    
    float angle1 : register(C10);
    float angle2 : register(C11);
    float angle3 : register(C12);
    float angle4 : register(C13);
    float angle5 : register(C14);
    float angle6 : register(C15);
    float angle7 : register(C16);
    float angle8 : register(C17);
    float angle9 : register(C18);
    float angle10 : register(C19);
    float angle11 : register(C20);
    float angle12 : register(C21);
    float angle13 : register(C22);
    float angle14 : register(C23);
    float angle15 : register(C24);
    float angle16 : register(C25);
    float angle17 : register(C26);
    float angle18 : register(C27);
    float angle19 : register(C28);
    float angle20 : register(C29);
    
    float4 color1 : register(C50);
    float4 color2 : register(C51);
    float4 color3 : register(C52);
    float4 color4 : register(C53);
    float4 color5 : register(C54);
    float4 color6 : register(C55);
    float4 color7 : register(C56);
    float4 color8 : register(C57);
    float4 color9 : register(C58);
    float4 color10 : register(C59);
    float4 color11 : register(C60);
    float4 color12 : register(C61);
    float4 color13 : register(C62);
    float4 color14 : register(C63);
    float4 color15 : register(C64);
    float4 color16 : register(C65);
    float4 color17 : register(C66);
    float4 color18 : register(C67);
    float4 color19 : register(C68);
    float4 color20 : register(C69);
    
    float4 main(float2 uv : TEXCOORD) : COLOR
    {
        float4 src = tex2D(inputSampler, uv);
        float2 p = float2(centerPoint)-uv;
        float angle = atan2(p.x, p.y) + 3.141596;
    
        float startAngle;
        float endAngle;
        float4 startColor;
        float4 endColor;
    
        if (angle >= angle1 && angle < angle2)
        {
            startAngle = angle1;
            startColor = color1;
    
            endAngle = angle2;
            endColor = color2;
        }
        else if (angle >= angle2 && angle < angle3)
        {
            startAngle = angle2;
            startColor = color2;
    
            endAngle = angle3;
            endColor = color3;
        }
        else if (angle >= angle3 && angle < angle4)
        {
            startAngle = angle3;
            startColor = color3;
    
            endAngle = angle4;
            endColor = color4;
        }
        else if (angle >= angle4 && angle < angle5)
        {
            startAngle = angle4;
            startColor = color4;
    
            endAngle = angle5;
            endColor = color5;
        }
        else if (angle >= angle5 && angle <= angle6)
        {
            startAngle = angle5;
            startColor = color5;
    
            endAngle = angle6;
            endColor = color6;
        }
        else if (angle >= angle6 && angle <= angle7)
        {
            startAngle = angle6;
            startColor = color6;
    
            endAngle = angle7;
            endColor = color7;
        }
        else if (angle >= angle7 && angle <= angle8)
        {
            startAngle = angle7;
            startColor = color7;
    
            endAngle = angle8;
            endColor = color8;
        }
        else if (angle >= angle8 && angle <= angle9)
        {
            startAngle = angle8;
            startColor = color8;
    
            endAngle = angle9;
            endColor = color9;
        }
        else if (angle >= angle9 && angle <= angle10)
        {
            startAngle = angle9;
            startColor = color9;
    
            endAngle = angle10;
            endColor = color10;
        }
        else if (angle >= angle10 && angle <= angle11)
        {
            startAngle = angle10;
            startColor = color10;
    
            endAngle = angle11;
            endColor = color11;
        }
        else if (angle >= angle11 && angle <= angle12)
        {
            startAngle = angle11;
            startColor = color11;
    
            endAngle = angle12;
            endColor = color12;
        }
        else if (angle >= angle12 && angle <= angle13)
        {
            startAngle = angle12;
            startColor = color12;
    
            endAngle = angle13;
            endColor = color13;
        }
        else if (angle >= angle13 && angle <= angle14)
        {
            startAngle = angle13;
            startColor = color13;
    
            endAngle = angle14;
            endColor = color14;
        }
        else if (angle >= angle14 && angle <= angle15)
        {
            startAngle = angle14;
            startColor = color14;
    
            endAngle = angle15;
            endColor = color15;
        }
        else if (angle >= angle15 && angle <= angle16)
        {
            startAngle = angle15;
            startColor = color15;
    
            endAngle = angle16;
            endColor = color16;
        }
        else if (angle >= angle16 && angle <= angle17)
        {
            startAngle = angle16;
            startColor = color16;
    
            endAngle = angle17;
            endColor = color17;
        }
        else if (angle >= angle17 && angle <= angle18)
        {
            startAngle = angle17;
            startColor = color17;
    
            endAngle = angle18;
            endColor = color18;
        }
        else if (angle >= angle18 && angle <= angle19)
        {
            startAngle = angle18;
            startColor = color18;
    
            endAngle = angle19;
            endColor = color19;
        }
        else if (angle >= angle19 && angle <= angle20)
        {
            startAngle = angle19;
            startColor = color19;
    
            endAngle = angle20;
            endColor = color20;
        }
    
        float offset = (angle - startAngle) / (endAngle - startAngle);
        float4 color = lerp(startColor, endColor, offset);
    
        // Multiply by the transparency of the source pixel 
        float3 output = color.rgb * src.a;
    
        return float4(output, src.a);
    }
    

    HLSL appears to support arrays in floating point constant buffers, but WPF doesn't. If it did, the shader could be rewritten to easily support an arbitrary number of stops and would be considerable more elegant.

    Resources: