diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index f4ebd33c3f5..46c06d2672c 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -886,146 +886,164 @@ private static void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect t 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; + } } } diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index c70d5a2c9bf..bffacf4543e 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -19,6 +19,32 @@ public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush") { } + [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() { diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png new file mode 100644 index 00000000000..f36b6360833 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png differ diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png new file mode 100644 index 00000000000..efdf280e669 Binary files /dev/null and b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_Partial_Cover.expected.png differ