diff --git a/src/Avalonia.Visuals/Media/ConicGradientBrush.cs b/src/Avalonia.Visuals/Media/ConicGradientBrush.cs new file mode 100644 index 0000000000..7c1266fa17 --- /dev/null +++ b/src/Avalonia.Visuals/Media/ConicGradientBrush.cs @@ -0,0 +1,55 @@ +using Avalonia.Media.Immutable; + +namespace Avalonia.Media +{ + /// + /// Paints an area with a swept circular gradient. + /// + public sealed class ConicGradientBrush : GradientBrush, IConicGradientBrush + { + /// + /// Defines the property. + /// + public static readonly StyledProperty CenterProperty = + AvaloniaProperty.Register( + nameof(Center), + RelativePoint.Center); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AngleProperty = + AvaloniaProperty.Register( + nameof(Angle), + 0); + + static ConicGradientBrush() + { + AffectsRender(CenterProperty, AngleProperty); + } + + /// + /// Gets or sets the center point of the gradient. + /// + public RelativePoint Center + { + get { return GetValue(CenterProperty); } + set { SetValue(CenterProperty, value); } + } + + /// + /// Gets or sets the angle of the start and end of the sweep, measured from above the center point. + /// + public double Angle + { + get { return GetValue(AngleProperty); } + set { SetValue(AngleProperty, value); } + } + + /// + public override IBrush ToImmutable() + { + return new ImmutableConicGradientBrush(this); + } + } +} diff --git a/src/Avalonia.Visuals/Media/IConicGradientBrush.cs b/src/Avalonia.Visuals/Media/IConicGradientBrush.cs new file mode 100644 index 0000000000..5368dd1851 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IConicGradientBrush.cs @@ -0,0 +1,19 @@ +namespace Avalonia.Media +{ + /// + /// Paints an area with a conic gradient. + /// + public interface IConicGradientBrush : IGradientBrush + { + /// + /// Gets the center point for the gradient. + /// + RelativePoint Center { get; } + + /// + /// Gets the starting angle for the gradient in degrees, measured from + /// the point above the center point. + /// + double Angle { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs new file mode 100644 index 0000000000..d3c80dfcad --- /dev/null +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.Immutable +{ + /// + /// A brush that draws with a sweep gradient. + /// + public class ImmutableConicGradientBrush : ImmutableGradientBrush, IConicGradientBrush + { + /// + /// Initializes a new instance of the class. + /// + /// The gradient stops. + /// The opacity of the brush. + /// The spread method. + /// The center point for the gradient. + /// The starting angle for the gradient. + public ImmutableConicGradientBrush( + IReadOnlyList gradientStops, + double opacity = 1, + GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, + RelativePoint? center = null, + double angle = 0) + : base(gradientStops, opacity, spreadMethod) + { + Center = center ?? RelativePoint.Center; + Angle = angle; + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush from which this brush's properties should be copied. + public ImmutableConicGradientBrush(ConicGradientBrush source) + : base(source) + { + Center = source.Center; + Angle = source.Angle; + } + + /// + public RelativePoint Center { get; } + + /// + public double Angle { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index a32b3327c2..44e0c82110 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -641,6 +641,23 @@ namespace Avalonia.Skia } } + break; + } + case IConicGradientBrush conicGradient: + { + var center = conicGradient.Center.ToPixels(targetSize).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); + + using (var shader = + SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation)) + { + paintWrapper.Paint.Shader = shader; + } + break; } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index ace658654d..136ff63f3d 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -423,6 +423,7 @@ namespace Avalonia.Direct2D1.Media var solidColorBrush = brush as ISolidColorBrush; var linearGradientBrush = brush as ILinearGradientBrush; var radialGradientBrush = brush as IRadialGradientBrush; + var conicGradientBrush = brush as IConicGradientBrush; var imageBrush = brush as IImageBrush; var visualBrush = brush as IVisualBrush; @@ -438,6 +439,11 @@ namespace Avalonia.Direct2D1.Media { return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationSize); } + else if (conicGradientBrush != null) + { + // there is no Direct2D implementation of Conic Gradients so use Radial as a stand-in + return new SolidColorBrushImpl(conicGradientBrush, _deviceContext); + } else if (imageBrush?.Source != null) { return new ImageBrushImpl( diff --git a/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs index f93b4a2e08..fea1ca9157 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/SolidColorBrushImpl.cs @@ -16,5 +16,22 @@ namespace Avalonia.Direct2D1.Media } ); } + + /// + /// Direct2D has no ConicGradient implementation so fall back to a solid colour brush based on + /// the first gradient stop. + /// + public SolidColorBrushImpl(IConicGradientBrush brush, SharpDX.Direct2D1.DeviceContext target) + { + PlatformBrush = new SharpDX.Direct2D1.SolidColorBrush( + target, + brush?.GradientStops[0].Color.ToDirect2D() ?? new SharpDX.Mathematics.Interop.RawColor4(), + new SharpDX.Direct2D1.BrushProperties + { + Opacity = brush != null ? (float)brush.Opacity : 1.0f, + Transform = target.Transform + } + ); + } } } diff --git a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs new file mode 100644 index 0000000000..db69a0a028 --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs @@ -0,0 +1,178 @@ +using Avalonia.Controls; +using Avalonia.Media; +using System.Threading.Tasks; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class ConicGradientBrushTests : TestBase + { + public ConicGradientBrushTests() : base(@"Media\ConicGradientBrush") + { + } + + [Fact] + public async Task ConicGradientBrush_RedBlue() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new ConicGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task ConicGradientBrush_RedBlue_Rotation() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new ConicGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + Angle = 90 + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task ConicGradientBrush_RedBlue_Center() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new ConicGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + Center = new RelativePoint(0.25, 0.25, RelativeUnit.Relative) + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task ConicGradientBrush_RedBlue_Center_and_Rotation() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new ConicGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + Center = new RelativePoint(0.25, 0.25, RelativeUnit.Relative), + Angle = 90 + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task ConicGradientBrush_RedBlue_SoftEdge() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new ConicGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 0.5 }, + new GradientStop { Color = Colors.Red, Offset = 1 }, + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task ConicGradientBrush_Umbrella() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new ConicGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Yellow, Offset = 0.1667 }, + new GradientStop { Color = Colors.Lime, Offset = 0.3333 }, + new GradientStop { Color = Colors.Aqua, Offset = 0.5000 }, + new GradientStop { Color = Colors.Blue, Offset = 0.6667 }, + new GradientStop { Color = Colors.Magenta, Offset = 0.8333 }, + new GradientStop { Color = Colors.Red, Offset = 1 }, + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png new file mode 100644 index 0000000000..8e08d02f66 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png new file mode 100644 index 0000000000..8e08d02f66 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png new file mode 100644 index 0000000000..8e08d02f66 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png new file mode 100644 index 0000000000..8e08d02f66 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png new file mode 100644 index 0000000000..8e08d02f66 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png new file mode 100644 index 0000000000..8e08d02f66 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png new file mode 100644 index 0000000000..563bbfcc61 Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png new file mode 100644 index 0000000000..424ac52059 Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png new file mode 100644 index 0000000000..a8719e2e70 Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Center_and_Rotation.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png new file mode 100644 index 0000000000..1c79db413b Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_Rotation.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png new file mode 100644 index 0000000000..4cf8eddd81 Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_RedBlue_SoftEdge.expected.png differ diff --git a/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png new file mode 100644 index 0000000000..98dfda782d Binary files /dev/null and b/tests/TestFiles/Skia/Media/ConicGradientBrush/ConicGradientBrush_Umbrella.expected.png differ