Browse Source

[Skia] Fix RadialGradientBrush for non center origin (#17925)

* Skia - Only reverse radial gradient stops if we cover the whole content bounds

* Skia - Only reverse radial gradient stops if we reach cover the whole content bounds

* Add some new lines
Benedikt Stebner 11 months ago
parent
commit
d3b61cd75e

+ 134 - 116
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -886,146 +886,164 @@ namespace Avalonia.Skia
             switch (gradientBrush)
             {
                 case ILinearGradientBrush linearGradient:
-                {
-                    var start = linearGradient.StartPoint.ToPixels(targetRect).ToSKPoint();
-                    var end = linearGradient.EndPoint.ToPixels(targetRect).ToSKPoint();
-
-                    // would be nice to cache these shaders possibly?
-                    if (linearGradient.Transform is null)
                     {
-                        using (var shader =
-                            SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
+                        var start = linearGradient.StartPoint.ToPixels(targetRect).ToSKPoint();
+                        var end = linearGradient.EndPoint.ToPixels(targetRect).ToSKPoint();
+
+                        // would be nice to cache these shaders possibly?
+                        if (linearGradient.Transform is null)
                         {
-                            paintWrapper.Paint.Shader = shader;
+                            using (var shader =
+                                SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
+                            {
+                                paintWrapper.Paint.Shader = shader;
+                            }
                         }
-                    }
-                    else
-                    {
-                        var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetRect);
-                        var offset = Matrix.CreateTranslation(transformOrigin);
-                        var transform = (-offset) * linearGradient.Transform.Value * (offset);
-
-                        using (var shader =
-                            SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
+                        else
                         {
-                            paintWrapper.Paint.Shader = shader;
-                        }   
-                    }
+                            var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetRect);
+                            var offset = Matrix.CreateTranslation(transformOrigin);
+                            var transform = (-offset) * linearGradient.Transform.Value * (offset);
 
-                    break;
-                }
-                case IRadialGradientBrush radialGradient:
-                {
-                    var centerPoint = radialGradient.Center.ToPixels(targetRect);
-                    var center = centerPoint.ToSKPoint();
-                    
-                    var radiusX = (radialGradient.RadiusX.ToValue(targetRect.Width));
-                    var radiusY = (radialGradient.RadiusY.ToValue(targetRect.Height));
+                            using (var shader =
+                                SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
+                            {
+                                paintWrapper.Paint.Shader = shader;
+                            }
+                        }
 
-                    var originPoint = radialGradient.GradientOrigin.ToPixels(targetRect);
-                    
-                    Matrix? transform = null;
-                    
-                    if (radiusX != radiusY)
-                        transform =
-                            Matrix.CreateTranslation(-centerPoint)
-                            * Matrix.CreateScale(1, radiusY / radiusX)
-                            * Matrix.CreateTranslation(centerPoint);
-                    
-                    
-                    if (radialGradient.Transform != null)
-                    {
-                        var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetRect);
-                        var offset = Matrix.CreateTranslation(transformOrigin);
-                        var brushTransform = (-offset) * radialGradient.Transform.Value * (offset);
-                        transform = transform.HasValue ? transform * brushTransform : brushTransform;
+                        break;
                     }
-                    
-                    if (originPoint.Equals(centerPoint))
+                case IRadialGradientBrush radialGradient:
                     {
-                        // when the origin is the same as the center the Skia RadialGradient acts the same as D2D
-                        using (var shader =
-                               transform.HasValue
-                                   ? SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode,
-                                       transform.Value.ToSKMatrix())
-                                   : SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode)
-                              )
+                        var centerPoint = radialGradient.Center.ToPixels(targetRect);
+                        var center = centerPoint.ToSKPoint();
+
+                        var radiusX = (radialGradient.RadiusX.ToValue(targetRect.Width));
+                        var radiusY = (radialGradient.RadiusY.ToValue(targetRect.Height));
+
+                        var originPoint = radialGradient.GradientOrigin.ToPixels(targetRect);
+
+                        Matrix? transform = null;
+
+                        if (radiusX != radiusY)
+                            transform =
+                                Matrix.CreateTranslation(-centerPoint)
+                                * Matrix.CreateScale(1, radiusY / radiusX)
+                                * Matrix.CreateTranslation(centerPoint);
+
+
+                        if (radialGradient.Transform != null)
                         {
-                            paintWrapper.Paint.Shader = shader;
+                            var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetRect);
+                            var offset = Matrix.CreateTranslation(transformOrigin);
+                            var brushTransform = (-offset) * radialGradient.Transform.Value * (offset);
+                            transform = transform.HasValue ? transform * brushTransform : brushTransform;
                         }
-                    }
-                    else
-                    {
-                        // when the origin is different to the center use a two point ConicalGradient to match the behaviour of D2D
 
-                        if (radiusX != radiusY)
-                            // Adjust the origin point for radiusX/Y transformation by reversing it
-                            originPoint = originPoint.WithY(
-                                (originPoint.Y - centerPoint.Y) * radiusX / radiusY + centerPoint.Y);
-                        
-                        var origin = originPoint.ToSKPoint();
-                        
-                        // reverse the order of the stops to match D2D
-                        var reversedColors = new SKColor[stopColors.Length];
-                        Array.Copy(stopColors, reversedColors, stopColors.Length);
-                        Array.Reverse(reversedColors);
-
-                        // and then reverse the reference point of the stops
-                        var reversedStops = new float[stopOffsets.Length];
-                        for (var i = 0; i < stopOffsets.Length; i++)
+                        if (originPoint.Equals(centerPoint))
                         {
-                            reversedStops[i] = stopOffsets[i];
-                            if (reversedStops[i] > 0 && reversedStops[i] < 1)
+                            // when the origin is the same as the center the Skia RadialGradient acts the same as D2D
+                            using (var shader =
+                                       transform.HasValue
+                                           ? SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode,
+                                               transform.Value.ToSKMatrix())
+                                           : SKShader.CreateRadialGradient(center, (float)radiusX, stopColors, stopOffsets, tileMode)
+                                      )
                             {
-                                reversedStops[i] = Math.Abs(1 - stopOffsets[i]);
+                                paintWrapper.Paint.Shader = shader;
                             }
                         }
-                            
-                        // compose with a background colour of the final stop to match D2D's behaviour of filling with the final color
-                        using (var shader = SKShader.CreateCompose(
-                                   SKShader.CreateColor(reversedColors[0]),
-                                   transform.HasValue
-                                       ? SKShader.CreateTwoPointConicalGradient(center, (float)radiusX, origin, 0,
-                                           reversedColors, reversedStops, tileMode, transform.Value.ToSKMatrix())
-                                       : SKShader.CreateTwoPointConicalGradient(center, (float)radiusX, origin, 0,
-                                           reversedColors, reversedStops, tileMode)
-
-                               )
-                              )
+                        else
                         {
-                            paintWrapper.Paint.Shader = shader;
+                            // when the origin is different to the center use a two point ConicalGradient to match the behaviour of D2D
+                            if (radiusX != radiusY)
+                                // Adjust the origin point for radiusX/Y transformation by reversing it
+                                originPoint = originPoint.WithY(
+                                    (originPoint.Y - centerPoint.Y) * radiusX / radiusY + centerPoint.Y);
+
+                            var origin = originPoint.ToSKPoint();
+                            var endOffset = 0.0;
+
+                            // and then reverse the reference point of the stops
+                            var reversedStops = new float[stopOffsets.Length];
+
+                            for (var i = 0; i < stopOffsets.Length; i++)
+                            {
+                                var offset = stopOffsets[i];
+                                if (endOffset < offset)
+                                {
+                                    endOffset = offset;
+                                }
+                                reversedStops[i] = offset;
+                                if (reversedStops[i] > 0 && reversedStops[i] < 1)
+                                {
+                                    reversedStops[i] = Math.Abs(1 - offset);
+                                }
+                            }
+
+                            var start = origin;
+                            var radiusStart = 0f;
+                            var end = center;
+                            var radiusEnd = (float)radiusX;
+                            var reverse = MathUtilities.AreClose(1, endOffset);
+
+                            if (reverse)
+                            {
+                                (start, radiusStart, end, radiusEnd) = (end, radiusEnd, start, radiusStart);
+
+                                // reverse the order of the stops to match D2D
+                                var reversedColors = new SKColor[stopColors.Length];
+                                Array.Copy(stopColors, reversedColors, stopColors.Length);
+                                Array.Reverse(reversedColors);
+                                stopColors = reversedColors;
+                                stopOffsets = reversedStops;
+                            }
+
+                            // compose with a background colour of the final stop to match D2D's behaviour of filling with the final color
+                            using (var shader = SKShader.CreateCompose(
+                                       SKShader.CreateColor(stopColors[0]),
+                                       transform.HasValue
+                                           ? SKShader.CreateTwoPointConicalGradient(start, radiusStart, end, radiusEnd,
+                                              stopColors, stopOffsets, tileMode, transform.Value.ToSKMatrix())
+                                           : SKShader.CreateTwoPointConicalGradient(start, radiusStart, end, radiusEnd,
+                                              stopColors, stopOffsets, tileMode)
+                                        )
+                                    )
+                            {
+                                paintWrapper.Paint.Shader = shader;
+                            }
                         }
-                    }
 
-                    break;
-                }
+                        break;
+                    }
                 case IConicGradientBrush conicGradient:
-                {
-                    var center = conicGradient.Center.ToPixels(targetRect).ToSKPoint();
+                    {
+                        var center = conicGradient.Center.ToPixels(targetRect).ToSKPoint();
 
-                    // Skia's default is that angle 0 is from the right hand side of the center point
-                    // but we are matching CSS where the vertical point above the center is 0.
-                    var angle = (float)(conicGradient.Angle - 90);
-                    var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y);
+                        // Skia's default is that angle 0 is from the right hand side of the center point
+                        // but we are matching CSS where the vertical point above the center is 0.
+                        var angle = (float)(conicGradient.Angle - 90);
+                        var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y);
 
-                    if (conicGradient.Transform is { })
-                    {
-                            
-                        var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetRect);
-                        var offset = Matrix.CreateTranslation(transformOrigin);
-                        var transform = (-offset) * conicGradient.Transform.Value * (offset);
+                        if (conicGradient.Transform is { })
+                        {
 
-                        rotation = rotation.PreConcat(transform.ToSKMatrix());
-                    }
+                            var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetRect);
+                            var offset = Matrix.CreateTranslation(transformOrigin);
+                            var transform = (-offset) * conicGradient.Transform.Value * (offset);
 
-                    using (var shader = 
-                        SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation))
-                    {
-                        paintWrapper.Paint.Shader = shader;
-                    }
+                            rotation = rotation.PreConcat(transform.ToSKMatrix());
+                        }
 
-                    break;
-                }
+                        using (var shader =
+                            SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation))
+                        {
+                            paintWrapper.Paint.Shader = shader;
+                        }
+
+                        break;
+                    }
             }
         }
 

+ 26 - 0
tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs

@@ -19,6 +19,32 @@ namespace Avalonia.Direct2D1.RenderTests.Media
         {
         }
 
+        [Fact]
+        public async Task RadialGradientBrush_Partial_Cover()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 200,
+                Height = 200,
+                Child = new Border
+                {
+                    Background = new RadialGradientBrush
+                    {
+                        GradientStops =
+                        {
+                            new GradientStop { Color = Colors.White, Offset = 0 },
+                            new GradientStop { Color = Color.Parse("#00DD00"), Offset = 0.7 }
+                        },
+                        GradientOrigin = new RelativePoint(0.7, 0.15, RelativeUnit.Relative)
+                    }
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
         [Fact]
         public async Task RadialGradientBrush_RedBlue()
         {

BIN
tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png


BIN
tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png