diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 4a8fb819ca..923b51814f 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -66,5 +66,8 @@ + + + diff --git a/samples/RenderDemo/Pages/BrushesPage.axaml b/samples/RenderDemo/Pages/BrushesPage.axaml new file mode 100644 index 0000000000..9d5b6ee987 --- /dev/null +++ b/samples/RenderDemo/Pages/BrushesPage.axaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/RenderDemo/Pages/BrushesPage.axaml.cs b/samples/RenderDemo/Pages/BrushesPage.axaml.cs new file mode 100644 index 0000000000..5852b72d9e --- /dev/null +++ b/samples/RenderDemo/Pages/BrushesPage.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace RenderDemo.Pages; + +public class BrushesPage : UserControl +{ + public BrushesPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} + diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs index 0979de16d0..5e97635c9a 100644 --- a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs @@ -30,6 +30,7 @@ namespace Avalonia.Animation.Animators return new ImmutableRadialGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center), s_relativePointAnimator.Interpolate(progress, oldRadial.GradientOrigin, newRadial.GradientOrigin), @@ -39,6 +40,7 @@ namespace Avalonia.Animation.Animators return new ImmutableConicGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center), s_doubleAnimator.Interpolate(progress, oldConic.Angle, newConic.Angle)); @@ -47,6 +49,7 @@ namespace Avalonia.Animation.Animators return new ImmutableLinearGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), + oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint), s_relativePointAnimator.Interpolate(progress, oldLinear.EndPoint, newLinear.EndPoint)); @@ -98,16 +101,19 @@ namespace Avalonia.Animation.Animators case IRadialGradientBrush oldRadial: return new ImmutableRadialGradientBrush( CreateStopsFromSolidColorBrush(solidColorBrush, oldRadial.GradientStops), solidColorBrush.Opacity, + oldRadial.Transform is { } ? new ImmutableTransform(oldRadial.Transform.Value) : null, oldRadial.SpreadMethod, oldRadial.Center, oldRadial.GradientOrigin, oldRadial.Radius); case IConicGradientBrush oldConic: return new ImmutableConicGradientBrush( CreateStopsFromSolidColorBrush(solidColorBrush, oldConic.GradientStops), solidColorBrush.Opacity, + oldConic.Transform is { } ? new ImmutableTransform(oldConic.Transform.Value) : null, oldConic.SpreadMethod, oldConic.Center, oldConic.Angle); case ILinearGradientBrush oldLinear: return new ImmutableLinearGradientBrush( CreateStopsFromSolidColorBrush(solidColorBrush, oldLinear.GradientStops), solidColorBrush.Opacity, + oldLinear.Transform is { } ? new ImmutableTransform(oldLinear.Transform.Value) : null, oldLinear.SpreadMethod, oldLinear.StartPoint, oldLinear.EndPoint); default: diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index e93a2b5632..c02b9d8639 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -41,6 +41,8 @@ MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphIndices.set( MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice Avalonia.Media.GlyphRun.GlyphOffsets.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphOffsets.set(Avalonia.Utilities.ReadOnlySlice)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphTypeface.set(Avalonia.Media.GlyphTypeface)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.ITransform Avalonia.Media.IBrush.Transform' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.ITransform Avalonia.Media.IBrush.Transform.get()' is present in the implementation but not in the contract. CannotSealType : Type 'Avalonia.Media.Pen' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. MembersMustExist : Member 'protected void Avalonia.Media.Pen.AffectsRender(Avalonia.AvaloniaProperty[])' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Media.Pen.RaiseInvalidated(System.EventArgs)' does not exist in the implementation but it does exist in the contract. @@ -52,7 +54,13 @@ MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.IsTraili MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.TextPosition.set(System.Int32)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.Typeface..ctor(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.Typeface..ctor(System.String, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableConicGradientBrush..ctor(System.Collections.Generic.IReadOnlyList, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Immutable.ImmutableGradientBrush..ctor(System.Collections.Generic.IReadOnlyList, System.Double, Avalonia.Media.GradientSpreadMethod)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableLinearGradientBrush..ctor(System.Collections.Generic.IReadOnlyList, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable, System.Nullable)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableRadialGradientBrush..ctor(System.Collections.Generic.IReadOnlyList, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable, System.Nullable, System.Double)' does not exist in the implementation but it does exist in the contract. TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. +MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableSolidColorBrush..ctor(Avalonia.Media.Color, System.Double)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void Avalonia.Media.Immutable.ImmutableTileBrush..ctor(Avalonia.Media.AlignmentX, Avalonia.Media.AlignmentY, Avalonia.RelativeRect, System.Double, Avalonia.RelativeRect, Avalonia.Media.Stretch, Avalonia.Media.TileMode, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. @@ -153,4 +161,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphR MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract. -Total Issues: 154 +Total Issues: 162 diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs index 11fbe9393a..9d989979a7 100644 --- a/src/Avalonia.Visuals/Media/Brush.cs +++ b/src/Avalonia.Visuals/Media/Brush.cs @@ -18,13 +18,19 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityProperty = AvaloniaProperty.Register(nameof(Opacity), 1.0); + /// + /// Defines the property. + /// + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); + /// public event EventHandler? Invalidated; static Brush() { Animation.Animation.RegisterAnimator(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType)); - AffectsRender(OpacityProperty); + AffectsRender(OpacityProperty, TransformProperty); } /// @@ -36,6 +42,15 @@ namespace Avalonia.Media set { SetValue(OpacityProperty, value); } } + /// + /// Gets or sets the transform of the brush. + /// + public ITransform? Transform + { + get { return GetValue(TransformProperty); } + set { SetValue(TransformProperty, value); } + } + /// /// Parses a brush string. /// diff --git a/src/Avalonia.Visuals/Media/IBrush.cs b/src/Avalonia.Visuals/Media/IBrush.cs index 15b7681be4..830c066182 100644 --- a/src/Avalonia.Visuals/Media/IBrush.cs +++ b/src/Avalonia.Visuals/Media/IBrush.cs @@ -12,5 +12,10 @@ namespace Avalonia.Media /// Gets the opacity of the brush. /// double Opacity { get; } + + /// + /// Gets the transform of the brush. + /// + ITransform? Transform { get; } } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs index d3c80dfcad..4b97615c4c 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs @@ -12,16 +12,18 @@ namespace Avalonia.Media.Immutable /// /// The gradient stops. /// The opacity of the brush. + /// The transform 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, + ImmutableTransform? transform = null, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, RelativePoint? center = null, double angle = 0) - : base(gradientStops, opacity, spreadMethod) + : base(gradientStops, opacity, transform, spreadMethod) { Center = center ?? RelativePoint.Center; Angle = angle; diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs index 1f6e3bbcfd..f1e51687d0 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs @@ -12,14 +12,17 @@ namespace Avalonia.Media.Immutable /// /// The gradient stops. /// The opacity of the brush. + /// The transform of the brush. /// The spread method. protected ImmutableGradientBrush( IReadOnlyList gradientStops, double opacity, + ImmutableTransform? transform, GradientSpreadMethod spreadMethod) { GradientStops = gradientStops; Opacity = opacity; + Transform = transform; SpreadMethod = spreadMethod; } @@ -28,7 +31,7 @@ namespace Avalonia.Media.Immutable /// /// The brush from which this brush's properties should be copied. protected ImmutableGradientBrush(GradientBrush source) - : this(source.GradientStops.ToImmutable(), source.Opacity, source.SpreadMethod) + : this(source.GradientStops.ToImmutable(), source.Opacity, source.Transform?.ToImmutable(), source.SpreadMethod) { } @@ -39,6 +42,11 @@ namespace Avalonia.Media.Immutable /// public double Opacity { get; } + /// + /// Gets the transform of the brush. + /// + public ITransform? Transform { get; } + /// public GradientSpreadMethod SpreadMethod { get; } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs index 15da8f8b43..2d4fe45c8e 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs @@ -16,6 +16,7 @@ namespace Avalonia.Media.Immutable /// The vertical alignment of a tile in the destination. /// The rectangle on the destination in which to paint a tile. /// The opacity of the brush. + /// The transform of the brush. /// The rectangle of the source image that will be displayed. /// /// How the source rectangle will be stretched to fill the destination rect. @@ -28,6 +29,7 @@ namespace Avalonia.Media.Immutable AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, + ImmutableTransform? transform = null, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, @@ -37,6 +39,7 @@ namespace Avalonia.Media.Immutable alignmentY, destinationRect ?? RelativeRect.Fill, opacity, + transform, sourceRect ?? RelativeRect.Fill, stretch, tileMode, diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs index 912d77d763..64c0f9b44e 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs @@ -12,16 +12,18 @@ namespace Avalonia.Media.Immutable /// /// The gradient stops. /// The opacity of the brush. + /// The transform of the brush. /// The spread method. /// The start point for the gradient. /// The end point for the gradient. public ImmutableLinearGradientBrush( IReadOnlyList gradientStops, double opacity = 1, + ImmutableTransform? transform = null, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, RelativePoint? startPoint = null, RelativePoint? endPoint = null) - : base(gradientStops, opacity, spreadMethod) + : base(gradientStops, opacity, transform, spreadMethod) { StartPoint = startPoint ?? RelativePoint.TopLeft; EndPoint = endPoint ?? RelativePoint.BottomRight; diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs index e26fbab5f5..3da4bdd8e9 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs @@ -12,6 +12,7 @@ namespace Avalonia.Media.Immutable /// /// The gradient stops. /// The opacity of the brush. + /// The transform of the brush. /// The spread method. /// The start point for the gradient. /// @@ -23,11 +24,12 @@ namespace Avalonia.Media.Immutable public ImmutableRadialGradientBrush( IReadOnlyList gradientStops, double opacity = 1, + ImmutableTransform? transform = null, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, RelativePoint? center = null, RelativePoint? gradientOrigin = null, double radius = 0.5) - : base(gradientStops, opacity, spreadMethod) + : base(gradientStops, opacity, transform, spreadMethod) { Center = center ?? RelativePoint.Center; GradientOrigin = gradientOrigin ?? RelativePoint.Center; diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs index 46e1df4054..9b1b2500ef 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs @@ -12,10 +12,12 @@ namespace Avalonia.Media.Immutable /// /// The color to use. /// The opacity of the brush. - public ImmutableSolidColorBrush(Color color, double opacity = 1) + /// The transform of the brush. + public ImmutableSolidColorBrush(Color color, double opacity = 1, ImmutableTransform? transform = null) { Color = color; Opacity = opacity; + Transform = null; } /// @@ -32,7 +34,7 @@ namespace Avalonia.Media.Immutable /// /// The brush from which this brush's properties should be copied. public ImmutableSolidColorBrush(ISolidColorBrush source) - : this(source.Color, source.Opacity) + : this(source.Color, source.Opacity, source.Transform?.ToImmutable()) { } @@ -46,11 +48,16 @@ namespace Avalonia.Media.Immutable /// public double Opacity { get; } + /// + /// Gets the transform of the brush. + /// + public ITransform? Transform { get; } + public bool Equals(ImmutableSolidColorBrush? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Color.Equals(other.Color) && Opacity.Equals(other.Opacity); + return Color.Equals(other.Color) && Opacity.Equals(other.Opacity) && (Transform == null && other.Transform == null ? true : (Transform != null && Transform.Equals(other.Transform))); } public override bool Equals(object? obj) @@ -62,7 +69,7 @@ namespace Avalonia.Media.Immutable { unchecked { - return (Color.GetHashCode() * 397) ^ Opacity.GetHashCode(); + return (Color.GetHashCode() * 397) ^ Opacity.GetHashCode() ^ (Transform is null ? 0 : Transform.GetHashCode()); } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs index fd4d921516..1019751733 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs @@ -14,6 +14,7 @@ namespace Avalonia.Media.Immutable /// The vertical alignment of a tile in the destination. /// The rectangle on the destination in which to paint a tile. /// The opacity of the brush. + /// The transform of the brush. /// The rectangle of the source image that will be displayed. /// /// How the source rectangle will be stretched to fill the destination rect. @@ -25,6 +26,7 @@ namespace Avalonia.Media.Immutable AlignmentY alignmentY, RelativeRect destinationRect, double opacity, + ImmutableTransform? transform, RelativeRect sourceRect, Stretch stretch, TileMode tileMode, @@ -34,6 +36,7 @@ namespace Avalonia.Media.Immutable AlignmentY = alignmentY; DestinationRect = destinationRect; Opacity = opacity; + Transform = transform; SourceRect = sourceRect; Stretch = stretch; TileMode = tileMode; @@ -50,6 +53,7 @@ namespace Avalonia.Media.Immutable source.AlignmentY, source.DestinationRect, source.Opacity, + source.Transform?.ToImmutable(), source.SourceRect, source.Stretch, source.TileMode, @@ -69,6 +73,11 @@ namespace Avalonia.Media.Immutable /// public double Opacity { get; } + /// + /// Gets the transform of the brush. + /// + public ITransform? Transform { get; } + /// public RelativeRect SourceRect { get; } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTransform.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableTransform.cs new file mode 100644 index 0000000000..d5ff2b8317 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableTransform.cs @@ -0,0 +1,21 @@ +using Avalonia.VisualTree; + +namespace Avalonia.Media.Immutable +{ + /// + /// Represents a transform on an . + /// + public class ImmutableTransform : ITransform + { + public Matrix Value { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The transform matrix. + public ImmutableTransform(Matrix matrix) + { + Value = matrix; + } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs index 4c6aae08ab..0fd2905660 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs @@ -16,6 +16,7 @@ namespace Avalonia.Media.Immutable /// The vertical alignment of a tile in the destination. /// The rectangle on the destination in which to paint a tile. /// The opacity of the brush. + /// The transform of the brush. /// The rectangle of the source image that will be displayed. /// /// How the source rectangle will be stretched to fill the destination rect. @@ -28,6 +29,7 @@ namespace Avalonia.Media.Immutable AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, + ImmutableTransform? transform = null, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, @@ -37,6 +39,7 @@ namespace Avalonia.Media.Immutable alignmentY, destinationRect ?? RelativeRect.Fill, opacity, + transform, sourceRect ?? RelativeRect.Fill, stretch, tileMode, diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 60701ecfaf..023a8b9cdd 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Animation; using Avalonia.Animation.Animators; +using Avalonia.Media.Immutable; using Avalonia.VisualTree; namespace Avalonia.Media @@ -44,6 +45,15 @@ namespace Avalonia.Media Changed?.Invoke(this, EventArgs.Empty); } + /// + /// Converts a transform to an immutable transform. + /// + /// The immutable transform + public ImmutableTransform ToImmutable() + { + return new ImmutableTransform(this.Value); + } + /// /// Returns a String representing this transform matrix instance. /// diff --git a/src/Avalonia.Visuals/Media/TransformExtensions.cs b/src/Avalonia.Visuals/Media/TransformExtensions.cs new file mode 100644 index 0000000000..ccf2231ce2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TransformExtensions.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Media.Immutable; + +namespace Avalonia.Media +{ + /// + /// Extension methods for transform classes. + /// + public static class TransformExtensions + { + /// + /// Converts a transform to an immutable transform. + /// + /// The transform. + /// + /// The result of calling if the transform is mutable, + /// otherwise . + /// + public static ImmutableTransform ToImmutable(this ITransform transform) + { + _ = transform ?? throw new ArgumentNullException(nameof(transform)); + + return (transform as Transform)?.ToImmutable() ?? new ImmutableTransform(transform.Value); + } + } +} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index e695a9cb41..642808eacb 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -614,10 +614,21 @@ namespace Avalonia.Skia var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); // would be nice to cache these shaders possibly? - using (var shader = - SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) + if (linearGradient.Transform is null) { - paintWrapper.Paint.Shader = shader; + using (var shader = + SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + } + else + { + using (var shader = + SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, linearGradient.Transform.Value.ToSKMatrix())) + { + paintWrapper.Paint.Shader = shader; + } } break; @@ -632,10 +643,21 @@ namespace Avalonia.Skia if (origin.Equals(center)) { // when the origin is the same as the center the Skia RadialGradient acts the same as D2D - using (var shader = - SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + if (radialGradient.Transform is null) { - paintWrapper.Paint.Shader = shader; + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + } + else + { + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode, radialGradient.Transform.Value.ToSKMatrix())) + { + paintWrapper.Paint.Shader = shader; + } } } else @@ -659,12 +681,25 @@ namespace Avalonia.Skia } // 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]), - SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode) - )) + if (radialGradient.Transform is null) { - paintWrapper.Paint.Shader = shader; + using (var shader = SKShader.CreateCompose( + SKShader.CreateColor(reversedColors[0]), + SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode) + )) + { + paintWrapper.Paint.Shader = shader; + } + } + else + { + using (var shader = SKShader.CreateCompose( + SKShader.CreateColor(reversedColors[0]), + SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode, radialGradient.Transform.Value.ToSKMatrix()) + )) + { + paintWrapper.Paint.Shader = shader; + } } } @@ -679,6 +714,11 @@ namespace Avalonia.Skia var angle = (float)(conicGradient.Angle - 90); var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y); + if (conicGradient.Transform is { }) + { + rotation = rotation.PreConcat(conicGradient.Transform.Value.ToSKMatrix()); + } + using (var shader = SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation)) { @@ -751,6 +791,11 @@ namespace Avalonia.Skia tileTransform, SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + if (tileBrush.Transform is { }) + { + paintTransform = paintTransform.PreConcat(tileBrush.Transform.Value.ToSKMatrix()); + } + using (var shader = image.ToShader(tileX, tileY, paintTransform)) { paintWrapper.Paint.Shader = shader;