Przeglądaj źródła

Merge pull request #6344 from wieslawsoltes/BrushTransformProperty

Add Transform property for Brush class
Max Katz 3 lat temu
rodzic
commit
24be9eb325

+ 3 - 0
samples/RenderDemo/MainWindow.xaml

@@ -66,5 +66,8 @@
     <TabItem Header="Path Measurement">
       <pages:PathMeasurementPage />
     </TabItem>
+    <TabItem Header="Brushes">
+      <pages:BrushesPage />
+    </TabItem>
   </controls:HamburgerMenu>
 </Window>

+ 71 - 0
samples/RenderDemo/Pages/BrushesPage.axaml

@@ -0,0 +1,71 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="RenderDemo.Pages.BrushesPage">
+  <Canvas Background="White" Width="480" Height="360">
+    <Rectangle Canvas.Left="20" Canvas.Top="20" Width="440" Height="50">
+      <Rectangle.Fill>
+        <LinearGradientBrush StartPoint="0,0" EndPoint="410,0">
+          <LinearGradientBrush.Transform>
+            <TransformGroup>
+              <ScaleTransform ScaleX="0.5" />
+              <SkewTransform />
+              <RotateTransform />
+              <TranslateTransform X="5" Y="15" />
+            </TransformGroup>
+          </LinearGradientBrush.Transform>
+          <LinearGradientBrush.GradientStops>
+            <GradientStop Color="Blue" Offset="0" />
+            <GradientStop Color="Green" Offset="0.5" />
+            <GradientStop Color="Lime" Offset="1" />
+          </LinearGradientBrush.GradientStops>
+        </LinearGradientBrush>
+      </Rectangle.Fill>
+    </Rectangle>
+    <TextBlock Canvas.Left="20" Canvas.Top="70" FontSize="30" Text="scale(0.5) on gradient" />
+    <Rectangle Canvas.Left="20" Canvas.Top="110" Width="440" Height="50">
+      <Rectangle.Fill>
+        <RadialGradientBrush Center="0.0,0.0" GradientOrigin="0.0,0.0" Radius="0.13636364">
+          <RadialGradientBrush.Transform>
+            <TransformGroup>
+              <ScaleTransform />
+              <SkewTransform AngleX="45" />
+              <RotateTransform />
+              <TranslateTransform X="240" Y="45" />
+            </TransformGroup>
+          </RadialGradientBrush.Transform>
+          <RadialGradientBrush.GradientStops>
+            <GradientStop Color="Black" Offset="0" />
+            <GradientStop Color="#FFFFA500" Offset="1" />
+          </RadialGradientBrush.GradientStops>
+        </RadialGradientBrush>
+      </Rectangle.Fill>
+    </Rectangle>
+    <TextBlock Canvas.Left="20" Canvas.Top="160" FontSize="30" Text="skewX(45) on gradient" />
+    <Rectangle Canvas.Left="20" Canvas.Top="210" Width="440" Height="50">
+      <Rectangle.Fill>
+        <VisualBrush TileMode="Tile" SourceRect="0,0,20,20" DestinationRect="0,0,20,20" Stretch="None">
+          <VisualBrush.Transform>
+            <TransformGroup>
+              <ScaleTransform ScaleX="2" ScaleY="2" />
+              <SkewTransform AngleX="45" />
+              <RotateTransform />
+              <TranslateTransform X="5" Y="5" />
+            </TransformGroup>
+          </VisualBrush.Transform>
+          <VisualBrush.Visual>
+            <Canvas Width="20" Height="20">
+              <Rectangle Canvas.Left="0" Canvas.Top="0" Width="10" Height="10" Fill="Maroon" />
+              <Rectangle Canvas.Left="10" Canvas.Top="0" Width="10" Height="10" Fill="Green" />
+              <Rectangle Canvas.Left="0" Canvas.Top="10" Width="10" Height="10" Fill="Blue" />
+              <Rectangle Canvas.Left="10" Canvas.Top="10" Width="10" Height="10" Fill="Yellow" />
+            </Canvas>
+          </VisualBrush.Visual>
+        </VisualBrush>
+      </Rectangle.Fill>
+    </Rectangle>
+    <TextBlock Canvas.Left="20" Canvas.Top="260" FontSize="30" Text="scale(2), skewX(45) on pattern" />
+  </Canvas>
+</UserControl>

+ 18 - 0
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);
+    }
+}
+

+ 6 - 0
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:

+ 9 - 1
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.Vector> 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<Avalonia.Vector>)' 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<T>(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<Avalonia.Media.Immutable.ImmutableGradientStop>, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable<Avalonia.RelativePoint>, 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<Avalonia.Media.Immutable.ImmutableGradientStop>, 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<Avalonia.Media.Immutable.ImmutableGradientStop>, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable<Avalonia.RelativePoint>, System.Nullable<Avalonia.RelativePoint>)' 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<Avalonia.Media.Immutable.ImmutableGradientStop>, System.Double, Avalonia.Media.GradientSpreadMethod, System.Nullable<Avalonia.RelativePoint>, System.Nullable<Avalonia.RelativePoint>, 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<System.Char>, 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<System.Int32>)' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice<T>..ctor(System.ReadOnlyMemory<T>, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 154
+Total Issues: 162

+ 16 - 1
src/Avalonia.Visuals/Media/Brush.cs

@@ -18,13 +18,19 @@ namespace Avalonia.Media
         public static readonly StyledProperty<double> OpacityProperty =
             AvaloniaProperty.Register<Brush, double>(nameof(Opacity), 1.0);
 
+        /// <summary>
+        /// Defines the <see cref="Transform"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ITransform?> TransformProperty =
+            AvaloniaProperty.Register<Brush, ITransform?>(nameof(Transform));
+
         /// <inheritdoc/>
         public event EventHandler? Invalidated;
 
         static Brush()
         {
             Animation.Animation.RegisterAnimator<BaseBrushAnimator>(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType));
-            AffectsRender<Brush>(OpacityProperty);
+            AffectsRender<Brush>(OpacityProperty, TransformProperty);
         }
 
         /// <summary>
@@ -36,6 +42,15 @@ namespace Avalonia.Media
             set { SetValue(OpacityProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the transform of the brush.
+        /// </summary>
+        public ITransform? Transform
+        {
+            get { return GetValue(TransformProperty); }
+            set { SetValue(TransformProperty, value); }
+        }
+
         /// <summary>
         /// Parses a brush string.
         /// </summary>

+ 5 - 0
src/Avalonia.Visuals/Media/IBrush.cs

@@ -12,5 +12,10 @@ namespace Avalonia.Media
         /// Gets the opacity of the brush.
         /// </summary>
         double Opacity { get; }
+
+        /// <summary>
+        /// Gets the transform of the brush.
+        /// </summary>
+        ITransform? Transform { get; }
     }
 }

+ 3 - 1
src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs

@@ -12,16 +12,18 @@ namespace Avalonia.Media.Immutable
         /// </summary>
         /// <param name="gradientStops">The gradient stops.</param>
         /// <param name="opacity">The opacity of the brush.</param>
+        /// <param name="transform">The transform of the brush.</param>
         /// <param name="spreadMethod">The spread method.</param>
         /// <param name="center">The center point for the gradient.</param>
         /// <param name="angle">The starting angle for the gradient.</param>
         public ImmutableConicGradientBrush(
             IReadOnlyList<ImmutableGradientStop> 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;

+ 9 - 1
src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs

@@ -12,14 +12,17 @@ namespace Avalonia.Media.Immutable
         /// </summary>
         /// <param name="gradientStops">The gradient stops.</param>
         /// <param name="opacity">The opacity of the brush.</param>
+        /// <param name="transform">The transform of the brush.</param>
         /// <param name="spreadMethod">The spread method.</param>
         protected ImmutableGradientBrush(
             IReadOnlyList<ImmutableGradientStop> gradientStops,
             double opacity,
+            ImmutableTransform? transform,
             GradientSpreadMethod spreadMethod)
         {
             GradientStops = gradientStops;
             Opacity = opacity;
+            Transform = transform;
             SpreadMethod = spreadMethod;
         }
 
@@ -28,7 +31,7 @@ namespace Avalonia.Media.Immutable
         /// </summary>
         /// <param name="source">The brush from which this brush's properties should be copied.</param>
         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
         /// <inheritdoc/>
         public double Opacity { get; }
 
+        /// <summary>
+        /// Gets the transform of the brush.
+        /// </summary>
+        public ITransform? Transform { get; }
+
         /// <inheritdoc/>
         public GradientSpreadMethod SpreadMethod { get; }
     }

+ 3 - 0
src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs

@@ -16,6 +16,7 @@ namespace Avalonia.Media.Immutable
         /// <param name="alignmentY">The vertical alignment of a tile in the destination.</param>
         /// <param name="destinationRect">The rectangle on the destination in which to paint a tile.</param>
         /// <param name="opacity">The opacity of the brush.</param>
+        /// <param name="transform">The transform of the brush.</param>
         /// <param name="sourceRect">The rectangle of the source image that will be displayed.</param>
         /// <param name="stretch">
         /// 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,

+ 3 - 1
src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs

@@ -12,16 +12,18 @@ namespace Avalonia.Media.Immutable
         /// </summary>
         /// <param name="gradientStops">The gradient stops.</param>
         /// <param name="opacity">The opacity of the brush.</param>
+        /// <param name="transform">The transform of the brush.</param>
         /// <param name="spreadMethod">The spread method.</param>
         /// <param name="startPoint">The start point for the gradient.</param>
         /// <param name="endPoint">The end point for the gradient.</param>
         public ImmutableLinearGradientBrush(
             IReadOnlyList<ImmutableGradientStop> 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;

+ 3 - 1
src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs

@@ -12,6 +12,7 @@ namespace Avalonia.Media.Immutable
         /// </summary>
         /// <param name="gradientStops">The gradient stops.</param>
         /// <param name="opacity">The opacity of the brush.</param>
+        /// <param name="transform">The transform of the brush.</param>
         /// <param name="spreadMethod">The spread method.</param>
         /// <param name="center">The start point for the gradient.</param>
         /// <param name="gradientOrigin">
@@ -23,11 +24,12 @@ namespace Avalonia.Media.Immutable
         public ImmutableRadialGradientBrush(
             IReadOnlyList<ImmutableGradientStop> 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;

+ 11 - 4
src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs

@@ -12,10 +12,12 @@ namespace Avalonia.Media.Immutable
         /// </summary>
         /// <param name="color">The color to use.</param>
         /// <param name="opacity">The opacity of the brush.</param>
-        public ImmutableSolidColorBrush(Color color, double opacity = 1)
+        /// <param name="transform">The transform of the brush.</param>
+        public ImmutableSolidColorBrush(Color color, double opacity = 1, ImmutableTransform? transform = null)
         {
             Color = color;
             Opacity = opacity;
+            Transform = null;
         }
 
         /// <summary>
@@ -32,7 +34,7 @@ namespace Avalonia.Media.Immutable
         /// </summary>
         /// <param name="source">The brush from which this brush's properties should be copied.</param>
         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
         /// </summary>
         public double Opacity { get; }
 
+        /// <summary>
+        /// Gets the transform of the brush.
+        /// </summary>
+        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());
             }
         }
 

+ 9 - 0
src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs

@@ -14,6 +14,7 @@ namespace Avalonia.Media.Immutable
         /// <param name="alignmentY">The vertical alignment of a tile in the destination.</param>
         /// <param name="destinationRect">The rectangle on the destination in which to paint a tile.</param>
         /// <param name="opacity">The opacity of the brush.</param>
+        /// <param name="transform">The transform of the brush.</param>
         /// <param name="sourceRect">The rectangle of the source image that will be displayed.</param>
         /// <param name="stretch">
         /// 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
         /// <inheritdoc/>
         public double Opacity { get; }
 
+        /// <summary>
+        /// Gets the transform of the brush.
+        /// </summary>
+        public ITransform? Transform { get; }
+
         /// <inheritdoc/>
         public RelativeRect SourceRect { get; }
 

+ 21 - 0
src/Avalonia.Visuals/Media/Immutable/ImmutableTransform.cs

@@ -0,0 +1,21 @@
+using Avalonia.VisualTree;
+
+namespace Avalonia.Media.Immutable
+{
+    /// <summary>
+    /// Represents a transform on an <see cref="IVisual"/>.
+    /// </summary>
+    public class ImmutableTransform : ITransform
+    {
+        public Matrix Value { get; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ImmutableTransform"/> class.
+        /// </summary>
+        /// <param name="matrix">The transform matrix.</param>
+        public ImmutableTransform(Matrix matrix)
+        {
+            Value = matrix;
+        }
+    }
+}

+ 3 - 0
src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs

@@ -16,6 +16,7 @@ namespace Avalonia.Media.Immutable
         /// <param name="alignmentY">The vertical alignment of a tile in the destination.</param>
         /// <param name="destinationRect">The rectangle on the destination in which to paint a tile.</param>
         /// <param name="opacity">The opacity of the brush.</param>
+        /// <param name="transform">The transform of the brush.</param>
         /// <param name="sourceRect">The rectangle of the source image that will be displayed.</param>
         /// <param name="stretch">
         /// 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,

+ 10 - 0
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);
         }
 
+        /// <summary>
+        /// Converts a transform to an immutable transform.
+        /// </summary>
+        /// <returns>The immutable transform</returns>
+        public ImmutableTransform ToImmutable()
+        {
+            return new ImmutableTransform(this.Value);
+        }
+
         /// <summary>
         /// Returns a String representing this transform matrix instance.
         /// </summary>

+ 26 - 0
src/Avalonia.Visuals/Media/TransformExtensions.cs

@@ -0,0 +1,26 @@
+using System;
+using Avalonia.Media.Immutable;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Extension methods for transform classes.
+    /// </summary>
+    public static class TransformExtensions
+    {
+        /// <summary>
+        /// Converts a transform to an immutable transform.
+        /// </summary>
+        /// <param name="transform">The transform.</param>
+        /// <returns>
+        /// The result of calling <see cref="Transform.ToImmutable"/> if the transform is mutable,
+        /// otherwise <paramref name="transform"/>.
+        /// </returns>
+        public static ImmutableTransform ToImmutable(this ITransform transform)
+        {
+            _ = transform ?? throw new ArgumentNullException(nameof(transform));
+
+            return (transform as Transform)?.ToImmutable() ?? new ImmutableTransform(transform.Value);
+        }
+    }
+}

+ 56 - 11
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;