diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml
index 4a53c9026f..1c2d768966 100644
--- a/samples/ControlCatalog/Pages/CarouselPage.xaml
+++ b/samples/ControlCatalog/Pages/CarouselPage.xaml
@@ -29,6 +29,7 @@
None
Slide
Crossfade
+ 3D Rotation
diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs
index 66180d4ccb..6b7707be13 100644
--- a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs
@@ -45,6 +45,9 @@ namespace ControlCatalog.Pages
case 2:
_carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25));
break;
+ case 3:
+ _carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
+ break;
}
}
}
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index 429c4776d5..e6e62f86ed 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -72,5 +72,8 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/Transform3DPage.axaml b/samples/RenderDemo/Pages/Transform3DPage.axaml
new file mode 100644
index 0000000000..30ed35bbac
--- /dev/null
+++ b/samples/RenderDemo/Pages/Transform3DPage.axaml
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+ I'm a text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/Transform3DPage.axaml.cs b/samples/RenderDemo/Pages/Transform3DPage.axaml.cs
new file mode 100644
index 0000000000..5083189c4c
--- /dev/null
+++ b/samples/RenderDemo/Pages/Transform3DPage.axaml.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using RenderDemo.ViewModels;
+
+namespace RenderDemo.Pages;
+
+public class Transform3DPage : UserControl
+{
+ public Transform3DPage()
+ {
+ InitializeComponent();
+ this.DataContext = new Transform3DPageViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
+
diff --git a/samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs b/samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs
new file mode 100644
index 0000000000..c8d1d40e3a
--- /dev/null
+++ b/samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs
@@ -0,0 +1,55 @@
+using System;
+using MiniMvvm;
+using Avalonia.Animation;
+
+namespace RenderDemo.ViewModels
+{
+ public class Transform3DPageViewModel : ViewModelBase
+ {
+ private double _depth = 200;
+
+ private double _centerX = 0;
+ private double _centerY = 0;
+ private double _centerZ = 0;
+ private double _angleX = 0;
+ private double _angleY = 0;
+ private double _angleZ = 0;
+
+ public double Depth
+ {
+ get => _depth;
+ set => RaiseAndSetIfChanged(ref _depth, value);
+ }
+
+ public double CenterX
+ {
+ get => _centerX;
+ set => RaiseAndSetIfChanged(ref _centerX, value);
+ }
+ public double CenterY
+ {
+ get => _centerY;
+ set => RaiseAndSetIfChanged(ref _centerY, value);
+ }
+ public double CenterZ
+ {
+ get => _centerZ;
+ set => RaiseAndSetIfChanged(ref _centerZ, value);
+ }
+ public double AngleX
+ {
+ get => _angleX;
+ set => RaiseAndSetIfChanged(ref _angleX, value);
+ }
+ public double AngleY
+ {
+ get => _angleY;
+ set => RaiseAndSetIfChanged(ref _angleY, value);
+ }
+ public double AngleZ
+ {
+ get => _angleZ;
+ set => RaiseAndSetIfChanged(ref _angleZ, value);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
index 34ec8ac503..e12ca722f9 100644
--- a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
+++ b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
@@ -43,6 +43,7 @@ namespace Avalonia.Animation.Animators
normalTransform.Children.Add(new SkewTransform());
normalTransform.Children.Add(new RotateTransform());
normalTransform.Children.Add(new TranslateTransform());
+ normalTransform.Children.Add(new Rotate3DTransform());
ctrl.RenderTransform = normalTransform;
}
diff --git a/src/Avalonia.Base/Animation/PageSlide.cs b/src/Avalonia.Base/Animation/PageSlide.cs
index 6f5d12d824..b22bc8b243 100644
--- a/src/Avalonia.Base/Animation/PageSlide.cs
+++ b/src/Avalonia.Base/Animation/PageSlide.cs
@@ -10,7 +10,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Animation
{
///
- /// Transitions between two pages by sliding them horizontally.
+ /// Transitions between two pages by sliding them horizontally or vertically.
///
public class PageSlide : IPageTransition
{
@@ -62,7 +62,7 @@ namespace Avalonia.Animation
public Easing SlideOutEasing { get; set; } = new LinearEasing();
///
- public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
+ public virtual async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
@@ -157,7 +157,7 @@ namespace Avalonia.Animation
///
/// Any one of the parameters may be null, but not both.
///
- private static IVisual GetVisualParent(IVisual? from, IVisual? to)
+ protected static IVisual GetVisualParent(IVisual? from, IVisual? to)
{
var p1 = (from ?? to)!.VisualParent;
var p2 = (to ?? from)!.VisualParent;
diff --git a/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
new file mode 100644
index 0000000000..60c7a97ced
--- /dev/null
+++ b/src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Media;
+using Avalonia.Styling;
+
+namespace Avalonia.Animation;
+
+public class Rotate3DTransition: PageSlide
+{
+
+ ///
+ /// Creates a new instance of the
+ ///
+ /// How long the rotation should take place
+ /// The orientation of the rotation
+ public Rotate3DTransition(TimeSpan duration, SlideAxis orientation = SlideAxis.Horizontal, double? depth = null)
+ : base(duration, orientation)
+ {
+ Depth = depth;
+ }
+
+ ///
+ /// Defines the depth of the 3D Effect. If null, depth will be calculated automatically from the width or height
+ /// of the common parent of the visual being rotated.
+ ///
+ public double? Depth { get; set; }
+
+ ///
+ /// Creates a new instance of the
+ ///
+ public Rotate3DTransition() { }
+
+ ///
+ public override async Task Start(Visual? @from, Visual? to, bool forward, CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ var tasks = new Task[from != null && to != null ? 2 : 1];
+ var parent = GetVisualParent(from, to);
+ var (rotateProperty, center) = Orientation switch
+ {
+ SlideAxis.Vertical => (Rotate3DTransform.AngleXProperty, parent.Bounds.Height),
+ SlideAxis.Horizontal => (Rotate3DTransform.AngleYProperty, parent.Bounds.Width),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ var depthSetter = new Setter {Property = Rotate3DTransform.DepthProperty, Value = Depth ?? center};
+ var centerZSetter = new Setter {Property = Rotate3DTransform.CenterZProperty, Value = -center / 2};
+
+ KeyFrame CreateKeyFrame(double cue, double rotation, int zIndex, bool isVisible = true) =>
+ new() {
+ Setters =
+ {
+ new Setter { Property = rotateProperty, Value = rotation },
+ new Setter { Property = Visual.ZIndexProperty, Value = zIndex },
+ new Setter { Property = Visual.IsVisibleProperty, Value = isVisible },
+ centerZSetter,
+ depthSetter
+ },
+ Cue = new Cue(cue)
+ };
+
+ if (from != null)
+ {
+ var animation = new Animation
+ {
+ Easing = SlideOutEasing,
+ Duration = Duration,
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ CreateKeyFrame(0d, 0d, 2),
+ CreateKeyFrame(0.5d, 45d * (forward ? -1 : 1), 1),
+ CreateKeyFrame(1d, 90d * (forward ? -1 : 1), 1, isVisible: false)
+ }
+ };
+
+ tasks[0] = animation.RunAsync(from, null, cancellationToken);
+ }
+
+ if (to != null)
+ {
+ to.IsVisible = true;
+ var animation = new Animation
+ {
+ Easing = SlideInEasing,
+ Duration = Duration,
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1),
+ CreateKeyFrame(0.5d, 45d * (forward ? 1 : -1), 1),
+ CreateKeyFrame(1d, 0d, 2)
+ }
+ };
+
+ tasks[from != null ? 1 : 0] = animation.RunAsync(to, null, cancellationToken);
+ }
+
+ await Task.WhenAll(tasks);
+
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ if (to != null)
+ {
+ to.ZIndex = 2;
+ }
+
+ if (from != null)
+ {
+ from.IsVisible = false;
+ from.ZIndex = 1;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs
index b08a0eb98a..5bbc657385 100644
--- a/src/Avalonia.Base/Matrix.cs
+++ b/src/Avalonia.Base/Matrix.cs
@@ -1,12 +1,22 @@
using System;
using System.Globalization;
+using System.Linq;
+using System.Numerics;
using Avalonia.Utilities;
namespace Avalonia
{
///
- /// A 2x3 matrix.
+ /// A 3x3 matrix.
///
+ /// Matrix layout:
+ /// | 1st col | 2nd col | 3r col |
+ /// 1st row | scaleX | skrewY | persX |
+ /// 2nd row | skrewX | scaleY | persY |
+ /// 3rd row | transX | transY | persZ |
+ ///
+ /// Note: Skia.SkMatrix uses a transposed layout (where for example skrewX/skrewY and perspp0/tranX are swapped).
+ ///
#if !BUILDTASK
public
#endif
@@ -14,40 +24,76 @@ namespace Avalonia
{
private readonly double _m11;
private readonly double _m12;
+ private readonly double _m13;
private readonly double _m21;
private readonly double _m22;
+ private readonly double _m23;
private readonly double _m31;
private readonly double _m32;
+ private readonly double _m33;
+
+ ///
+ /// Initializes a new instance of the struct (equivalent to a 2x3 Matrix without perspective).
+ ///
+ /// The first element of the first row.
+ /// The second element of the first row.
+ /// The first element of the second row.
+ /// The second element of the second row.
+ /// The first element of the third row.
+ /// The second element of the third row.
+ public Matrix(
+ double scaleX,
+ double skrewY,
+ double skrewX,
+ double scaleY,
+ double offsetX,
+ double offsetY) : this( scaleX, skrewY, 0, skrewX, scaleY, 0, offsetX, offsetY, 1)
+ {
+ }
+
+
///
/// Initializes a new instance of the struct.
///
- /// The first element of the first row.
- /// The second element of the first row.
- /// The first element of the second row.
- /// The second element of the second row.
+ /// The first element of the first row.
+ /// The second element of the first row.
+ /// The third element of the first row.
+ /// The first element of the second row.
+ /// The second element of the second row.
+ /// The third element of the second row.
/// The first element of the third row.
/// The second element of the third row.
+ /// The third element of the third row.
public Matrix(
- double m11,
- double m12,
- double m21,
- double m22,
+ double scaleX,
+ double skrewY,
+ double persX,
+ double skrewX,
+ double scaleY,
+ double persY,
double offsetX,
- double offsetY)
+ double offsetY,
+ double persZ)
{
- _m11 = m11;
- _m12 = m12;
- _m21 = m21;
- _m22 = m22;
+ _m11 = scaleX;
+ _m12 = skrewY;
+ _m13 = persX;
+ _m21 = skrewX;
+ _m22 = scaleY;
+ _m23 = persY;
_m31 = offsetX;
_m32 = offsetY;
+ _m33 = persZ;
}
///
/// Returns the multiplicative identity matrix.
///
- public static Matrix Identity { get; } = new Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
+ public static Matrix Identity { get; } = new Matrix(
+ 1.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0,
+ 0.0, 0.0, 1.0);
///
/// Returns whether the matrix is the identity matrix.
@@ -60,35 +106,50 @@ namespace Avalonia
public bool HasInverse => !MathUtilities.IsZero(GetDeterminant());
///
- /// The first element of the first row
+ /// The first element of the first row (scaleX).
///
public double M11 => _m11;
///
- /// The second element of the first row
+ /// The second element of the first row (skrewY).
///
public double M12 => _m12;
///
- /// The first element of the second row
+ /// The third element of the first row (persX: input x-axis perspective factor).
+ ///
+ public double M13 => _m13;
+
+ ///
+ /// The first element of the second row (skrewX).
///
public double M21 => _m21;
///
- /// The second element of the second row
+ /// The second element of the second row (scaleY).
///
public double M22 => _m22;
///
- /// The first element of the third row
+ /// The third element of the second row (persY: input y-axis perspective factor).
+ ///
+ public double M23 => _m23;
+
+ ///
+ /// The first element of the third row (offsetX/translateX).
///
public double M31 => _m31;
///
- /// The second element of the third row
+ /// The second element of the third row (offsetY/translateY).
///
public double M32 => _m32;
+ ///
+ /// The third element of the third row (persZ: perspective scale factor).
+ ///
+ public double M33 => _m33;
+
///
/// Multiplies two matrices together and returns the resulting matrix.
///
@@ -98,12 +159,15 @@ namespace Avalonia
public static Matrix operator *(Matrix value1, Matrix value2)
{
return new Matrix(
- (value1.M11 * value2.M11) + (value1.M12 * value2.M21),
- (value1.M11 * value2.M12) + (value1.M12 * value2.M22),
- (value1.M21 * value2.M11) + (value1.M22 * value2.M21),
- (value1.M21 * value2.M12) + (value1.M22 * value2.M22),
- (value1._m31 * value2.M11) + (value1._m32 * value2.M21) + value2._m31,
- (value1._m31 * value2.M12) + (value1._m32 * value2.M22) + value2._m32);
+ (value1.M11 * value2.M11) + (value1.M12 * value2.M21) + (value1.M13 * value2.M31),
+ (value1.M11 * value2.M12) + (value1.M12 * value2.M22) + (value1.M13 * value2.M32),
+ (value1.M11 * value2.M13) + (value1.M12 * value2.M23) + (value1.M13 * value2.M33),
+ (value1.M21 * value2.M11) + (value1.M22 * value2.M21) + (value1.M23 * value2.M31),
+ (value1.M21 * value2.M12) + (value1.M22 * value2.M22) + (value1.M23 * value2.M32),
+ (value1.M21 * value2.M13) + (value1.M22 * value2.M23) + (value1.M23 * value2.M33),
+ (value1.M31 * value2.M11) + (value1.M32 * value2.M21) + (value1.M33 * value2.M31),
+ (value1.M31 * value2.M12) + (value1.M32 * value2.M22) + (value1.M33 * value2.M32),
+ (value1.M31 * value2.M13) + (value1.M32 * value2.M23) + (value1.M33 * value2.M33));
}
///
@@ -171,7 +235,7 @@ namespace Avalonia
/// A scaling matrix.
public static Matrix CreateScale(double xScale, double yScale)
{
- return CreateScale(new Vector(xScale, yScale));
+ return new Matrix(xScale, 0, 0, yScale, 0, 0);
}
///
@@ -181,7 +245,7 @@ namespace Avalonia
/// A scaling matrix.
public static Matrix CreateScale(Vector scales)
{
- return new Matrix(scales.X, 0, 0, scales.Y, 0, 0);
+ return CreateScale(scales.X, scales.Y);
}
///
@@ -214,7 +278,7 @@ namespace Avalonia
{
return angle * 0.0174532925;
}
-
+
///
/// Appends another matrix as post-multiplication operation.
/// Equivalent to this * value;
@@ -227,7 +291,7 @@ namespace Avalonia
}
///
- /// Prpends another matrix as pre-multiplication operation.
+ /// Prepends another matrix as pre-multiplication operation.
/// Equivalent to value * this;
///
/// A matrix.
@@ -247,7 +311,49 @@ namespace Avalonia
///
public double GetDeterminant()
{
- return (_m11 * _m22) - (_m12 * _m21);
+ // implemented using "Laplace expansion":
+ return _m11 * (_m22 * _m33 - _m23 * _m32)
+ - _m12 * (_m21 * _m33 - _m23 * _m31)
+ + _m13 * (_m21 * _m32 - _m22 * _m31);
+ }
+
+ ///
+ /// Transforms the point with the matrix
+ ///
+ /// The point to be transformed
+ /// The transformed point
+ public Point Transform(Point p)
+ {
+ Point transformedResult;
+
+ // If this matrix contains a non-affine transform with need to extend
+ // the point to a 3D vector and flatten it back for 2d display
+ // by multiplying X and Y with the inverse of the Z axis.
+ // The code below also works with affine transformations, but for performance (and compatibility)
+ // reasons we will use the more complex calculation only if necessary
+ if (ContainsPerspective())
+ {
+ var m44 = new Matrix4x4(
+ (float)M11, (float)M12, (float)M13, 0,
+ (float)M21, (float)M22, (float)M23, 0,
+ (float)M31, (float)M32, (float)M33, 0,
+ 0, 0, 0, 1
+ );
+
+ var vector = new Vector3((float)p.X, (float)p.Y, 1);
+ var transformedVector = Vector3.Transform(vector, m44);
+ var z = 1 / transformedVector.Z;
+
+ transformedResult = new Point(transformedVector.X * z, transformedVector.Y * z);
+ }
+ else
+ {
+ return new Point(
+ (p.X * M11) + (p.Y * M21) + M31,
+ (p.X * M12) + (p.Y * M22) + M32);
+ }
+
+ return transformedResult;
}
///
@@ -260,10 +366,13 @@ namespace Avalonia
// ReSharper disable CompareOfFloatsByEqualityOperator
return _m11 == other.M11 &&
_m12 == other.M12 &&
+ _m13 == other.M13 &&
_m21 == other.M21 &&
_m22 == other.M22 &&
+ _m23 == other.M23 &&
_m31 == other.M31 &&
- _m32 == other.M32;
+ _m32 == other.M32 &&
+ _m33 == other.M33;
// ReSharper restore CompareOfFloatsByEqualityOperator
}
@@ -280,9 +389,18 @@ namespace Avalonia
/// The hash code.
public override int GetHashCode()
{
- return M11.GetHashCode() + M12.GetHashCode() +
- M21.GetHashCode() + M22.GetHashCode() +
- M31.GetHashCode() + M32.GetHashCode();
+ return (_m11, _m12, _m13, _m21, _m22, _m23, _m31, _m32, _m33).GetHashCode();
+ }
+
+ ///
+ /// Determines if the current matrix contains perspective (non-affine) transforms (true) or only (affine) transforms that could be mapped into an 2x3 matrix (false).
+ ///
+ public bool ContainsPerspective()
+ {
+
+ // ReSharper disable CompareOfFloatsByEqualityOperator
+ return _m13 != 0 || _m23 != 0 || _m33 != 1;
+ // ReSharper restore CompareOfFloatsByEqualityOperator
}
///
@@ -292,15 +410,25 @@ namespace Avalonia
public override string ToString()
{
CultureInfo ci = CultureInfo.CurrentCulture;
+
+ string msg;
+ double[] values;
+
+ if (ContainsPerspective())
+ {
+ msg = "{{ {{M11:{0} M12:{1} M13:{2}}} {{M21:{3} M22:{4} M23:{5}}} {{M31:{6} M32:{7} M33:{8}}} }}";
+ values = new[] { M11, M12, M13, M21, M22, M23, M31, M32, M33 };
+ }
+ else
+ {
+ msg = "{{ {{M11:{0} M12:{1}}} {{M21:{2} M22:{3}}} {{M31:{4} M32:{5}}} }}";
+ values = new[] { M11, M12, M21, M22, M31, M32 };
+ }
+
return string.Format(
ci,
- "{{ {{M11:{0} M12:{1}}} {{M21:{2} M22:{3}}} {{M31:{4} M32:{5}}} }}",
- M11.ToString(ci),
- M12.ToString(ci),
- M21.ToString(ci),
- M22.ToString(ci),
- M31.ToString(ci),
- M32.ToString(ci));
+ msg,
+ values.Select((v) => v.ToString(ci)).ToArray());
}
///
@@ -318,14 +446,20 @@ namespace Avalonia
return false;
}
+ var invdet = 1 / d;
+
inverted = new Matrix(
- _m22 / d,
- -_m12 / d,
- -_m21 / d,
- _m11 / d,
- ((_m21 * _m32) - (_m22 * _m31)) / d,
- ((_m12 * _m31) - (_m11 * _m32)) / d);
-
+ (_m22 * _m33 - _m32 * _m23) * invdet,
+ (_m13 * _m31 - _m12 * _m33) * invdet,
+ (_m12 * _m23 - _m13 * _m22) * invdet,
+ (_m23 * _m31 - _m21 * _m33) * invdet,
+ (_m11 * _m33 - _m13 * _m31) * invdet,
+ (_m21 * _m13 - _m11 * _m23) * invdet,
+ (_m21 * _m32 - _m31 * _m22) * invdet,
+ (_m21 * _m12 - _m11 * _m32) * invdet,
+ (_m11 * _m22 - _m21 * _m12) * invdet
+ );
+
return true;
}
@@ -336,7 +470,7 @@ namespace Avalonia
/// The inverted matrix.
public Matrix Invert()
{
- if (!TryInvert(out Matrix inverted))
+ if (!TryInvert(out var inverted))
{
throw new InvalidOperationException("Transform is not invertible.");
}
@@ -347,20 +481,30 @@ namespace Avalonia
///
/// Parses a string.
///
- /// Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new
+ /// Six or nine comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY[, persX, persY, persZ]) that describe the new
/// The .
public static Matrix Parse(string s)
{
+ // initialize to satisfy compiler - only used when retrieved from string.
+ double v8 = 0;
+ double v9 = 0;
+
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix."))
{
- return new Matrix(
- tokenizer.ReadDouble(),
- tokenizer.ReadDouble(),
- tokenizer.ReadDouble(),
- tokenizer.ReadDouble(),
- tokenizer.ReadDouble(),
- tokenizer.ReadDouble()
- );
+ var v1 = tokenizer.ReadDouble();
+ var v2 = tokenizer.ReadDouble();
+ var v3 = tokenizer.ReadDouble();
+ var v4 = tokenizer.ReadDouble();
+ var v5 = tokenizer.ReadDouble();
+ var v6 = tokenizer.ReadDouble();
+ var pers = tokenizer.TryReadDouble(out var v7);
+ pers = pers && tokenizer.TryReadDouble(out v8);
+ pers = pers && tokenizer.TryReadDouble(out v9);
+
+ if (pers)
+ return new Matrix(v1, v2, v7, v3, v4, v8, v5, v6, v9);
+ else
+ return new Matrix(v1, v2, v3, v4, v5, v6);
}
}
@@ -369,14 +513,14 @@ namespace Avalonia
///
/// Matrix to decompose.
/// Decomposed matrix.
- /// The status of the operation.
+ /// The status of the operation.
public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed)
{
decomposed = default;
var determinant = matrix.GetDeterminant();
- if (MathUtilities.IsZero(determinant))
+ if (MathUtilities.IsZero(determinant) || matrix.ContainsPerspective())
{
return false;
}
diff --git a/src/Avalonia.Base/Point.cs b/src/Avalonia.Base/Point.cs
index 67e7d71fbc..fbdf0db800 100644
--- a/src/Avalonia.Base/Point.cs
+++ b/src/Avalonia.Base/Point.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Numerics;
#if !BUILDTASK
using Avalonia.Animation.Animators;
#endif
@@ -168,12 +169,7 @@ namespace Avalonia
/// The point.
/// The matrix.
/// The resulting point.
- public static Point operator *(Point point, Matrix matrix)
- {
- return new Point(
- (point.X * matrix.M11) + (point.Y * matrix.M21) + matrix.M31,
- (point.X * matrix.M12) + (point.Y * matrix.M22) + matrix.M32);
- }
+ public static Point operator *(Point point, Matrix matrix) => matrix.Transform(point);
///
/// Parses a string.
@@ -242,18 +238,7 @@ namespace Avalonia
///
/// The transform.
/// The transformed point.
- public Point Transform(Matrix transform)
- {
- var x = X;
- var y = Y;
- var xadd = y * transform.M21 + transform.M31;
- var yadd = x * transform.M12 + transform.M32;
- x *= transform.M11;
- x += xadd;
- y *= transform.M22;
- y += yadd;
- return new Point(x, y);
- }
+ public Point Transform(Matrix transform) => transform.Transform(this);
///
/// Returns a new point with the specified X coordinate.
diff --git a/src/Avalonia.Base/Rotate3DTransform.cs b/src/Avalonia.Base/Rotate3DTransform.cs
new file mode 100644
index 0000000000..2c4e515861
--- /dev/null
+++ b/src/Avalonia.Base/Rotate3DTransform.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Numerics;
+using Avalonia.Animation.Animators;
+
+namespace Avalonia.Media;
+
+///
+/// Non-Affine 3D transformation for rotating a visual around a definable axis
+///
+public class Rotate3DTransform : Transform
+{
+ private readonly bool _isInitializing;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty AngleXProperty =
+ AvaloniaProperty.Register(nameof(AngleX));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty AngleYProperty =
+ AvaloniaProperty.Register(nameof(AngleY));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty AngleZProperty =
+ AvaloniaProperty.Register(nameof(AngleZ));
+
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty CenterXProperty =
+ AvaloniaProperty.Register(nameof(CenterX));
+
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty CenterYProperty =
+ AvaloniaProperty.Register(nameof(CenterY));
+
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty CenterZProperty =
+ AvaloniaProperty.Register(nameof(CenterZ));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty DepthProperty =
+ AvaloniaProperty.Register(nameof(Depth));
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Rotate3DTransform() { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The rotation around the X-Axis
+ /// The rotation around the Y-Axis
+ /// The rotation around the Z-Axis
+ /// The origin of the X-Axis
+ /// The origin of the Y-Axis
+ /// The origin of the Z-Axis
+ /// The depth of the 3D effect
+ public Rotate3DTransform(
+ double angleX,
+ double angleY,
+ double angleZ,
+ double centerX,
+ double centerY,
+ double centerZ,
+ double depth) : this()
+ {
+ _isInitializing = true;
+ AngleX = angleX;
+ AngleY = angleY;
+ AngleZ = angleZ;
+ CenterX = centerX;
+ CenterY = centerY;
+ CenterZ = centerZ;
+ Depth = depth;
+ _isInitializing = false;
+ }
+
+ ///
+ /// Sets the rotation around the X-Axis
+ ///
+ public double AngleX
+ {
+ get => GetValue(AngleXProperty);
+ set => SetValue(AngleXProperty, value);
+ }
+
+ ///
+ /// Sets the rotation around the Y-Axis
+ ///
+ public double AngleY
+ {
+ get => GetValue(AngleYProperty);
+ set => SetValue(AngleYProperty, value);
+ }
+
+ ///
+ /// Sets the rotation around the Z-Axis
+ ///
+ public double AngleZ
+ {
+ get => GetValue(AngleZProperty);
+ set => SetValue(AngleZProperty, value);
+ }
+
+ ///
+ /// Moves the origin the X-Axis rotates around
+ ///
+ public double CenterX
+ {
+ get => GetValue(CenterXProperty);
+ set => SetValue(CenterXProperty, value);
+ }
+
+ ///
+ /// Moves the origin the Y-Axis rotates around
+ ///
+ public double CenterY
+ {
+ get => GetValue(CenterYProperty);
+ set => SetValue(CenterYProperty, value);
+ }
+
+ ///
+ /// Moves the origin the Z-Axis rotates around
+ ///
+ public double CenterZ
+ {
+ get => GetValue(CenterZProperty);
+ set => SetValue(CenterZProperty, value);
+ }
+
+ ///
+ /// Affects the depth of the rotation effect
+ ///
+ public double Depth
+ {
+ get => GetValue(DepthProperty);
+ set => SetValue(DepthProperty, value);
+ }
+
+ ///
+ /// Gets the transform's .
+ ///
+ public override Matrix Value
+ {
+ get
+ {
+ var matrix44 = Matrix4x4.Identity;
+ //Copy values first, because it's not guaranteed, that values will not change during calculation
+ var (copyCenterX,
+ copyCenterY,
+ copyCenterZ,
+ copyAngleX,
+ copyAngleY,
+ copyAngleZ,
+ copyDepth) = (CenterX, CenterY, CenterZ, AngleX, AngleY, AngleZ, Depth);
+
+ var centerSum = copyCenterX + copyCenterY + copyCenterZ;
+
+ if (Math.Abs(centerSum) > double.Epsilon) matrix44 *= Matrix4x4.CreateTranslation(-(float)copyCenterX, -(float)copyCenterY, -(float)copyCenterZ);
+
+ if (copyAngleX != 0) matrix44 *= Matrix4x4.CreateRotationX((float)Matrix.ToRadians(copyAngleX));
+ if (copyAngleY != 0) matrix44 *= Matrix4x4.CreateRotationY((float)Matrix.ToRadians(copyAngleY));
+ if (copyAngleZ != 0) matrix44 *= Matrix4x4.CreateRotationZ((float)Matrix.ToRadians(copyAngleZ));
+
+ if (Math.Abs(centerSum) > double.Epsilon) matrix44 *= Matrix4x4.CreateTranslation((float)copyCenterX, (float)copyCenterY, (float)copyCenterZ);
+
+ if (copyDepth != 0)
+ {
+ var perspectiveMatrix = Matrix4x4.Identity;
+ perspectiveMatrix.M34 = -1 / (float)copyDepth;
+ matrix44 *= perspectiveMatrix;
+ }
+
+ var matrix = new Matrix(
+ matrix44.M11,
+ matrix44.M12,
+ matrix44.M14,
+ matrix44.M21,
+ matrix44.M22,
+ matrix44.M24,
+ matrix44.M41,
+ matrix44.M42,
+ matrix44.M44);
+
+ return matrix;
+ }
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ if (!_isInitializing) RaiseChanged();
+ }
+}
diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
index e9b99c9aa8..a801d338c3 100644
--- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
+++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
@@ -103,5 +103,6 @@
+
diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
index 166f2e8f98..d584216f17 100644
--- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
+++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
@@ -103,9 +103,9 @@ namespace Avalonia.Skia
SkewY = (float)m.M12,
ScaleY = (float)m.M22,
TransY = (float)m.M32,
- Persp0 = 0,
- Persp1 = 0,
- Persp2 = 1
+ Persp0 = (float)m.M13,
+ Persp1 = (float)m.M23,
+ Persp2 = (float)m.M33
};
return sm;
diff --git a/tests/Avalonia.Base.UnitTests/MatrixTests.cs b/tests/Avalonia.Base.UnitTests/MatrixTests.cs
new file mode 100644
index 0000000000..99fe4685d7
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/MatrixTests.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests;
+
+///
+/// These tests use the "official" Matrix4x4 and Matrix3x2 from the System.Numerics namespace, to validate
+/// that Avalonias own implementation of a 3x3 Matrix works correctly.
+///
+public class MatrixTests
+{
+ ///
+ /// Because Avalonia is working internally with doubles, but System.Numerics Vector and Matrix implementations
+ /// only make use of floats, we need to reduce precision, comparing them. It should be sufficient to compare
+ /// 5 fractional digits to ensure, that the result is correct.
+ ///
+ /// The expected vector
+ /// The actual transformed point
+ private void AssertCoordinatesEqualWithReducedPrecision(Vector2 expected, Point actual)
+ {
+ double ReducePrecision(double input) => Math.Truncate(input * 10000);
+
+ var expectedX = ReducePrecision(expected.X);
+ var expectedY = ReducePrecision(expected.Y);
+
+ var actualX = ReducePrecision(actual.X);
+ var actualY = ReducePrecision(actual.Y);
+
+ Assert.Equal(expectedX, actualX);
+ Assert.Equal(expectedY, actualY);
+ }
+
+ [Fact]
+ public void Transform_Point_Should_Return_Correct_Value_For_Translated_Matrix()
+ {
+ var vector2 = Vector2.Transform(
+ new Vector2(1, 1),
+ Matrix3x2.CreateTranslation(2, 2));
+ var expected = new Point(vector2.X, vector2.Y);
+
+ var matrix = Matrix.CreateTranslation(2, 2);
+ var point = new Point(1, 1);
+ var transformedPoint = matrix.Transform(point);
+
+ Assert.Equal(expected, transformedPoint);
+ }
+
+ [Fact]
+ public void Transform_Point_Should_Return_Correct_Value_For_Rotated_Matrix()
+ {
+ var expected = Vector2.Transform(
+ new Vector2(0, 10),
+ Matrix3x2.CreateRotation((float)Matrix.ToRadians(45)));
+
+ var matrix = Matrix.CreateRotation(Matrix.ToRadians(45));
+ var point = new Point(0, 10);
+ var actual = matrix.Transform(point);
+
+ AssertCoordinatesEqualWithReducedPrecision(expected, actual);
+ }
+
+ [Fact]
+ public void Transform_Point_Should_Return_Correct_Value_For_Scaled_Matrix()
+ {
+ var vector2 = Vector2.Transform(
+ new Vector2(1, 1),
+ Matrix3x2.CreateScale(2, 2));
+ var expected = new Point(vector2.X, vector2.Y);
+ var matrix = Matrix.CreateScale(2, 2);
+ var point = new Point(1, 1);
+ var actual = matrix.Transform(point);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void Transform_Point_Should_Return_Correct_Value_For_Skewed_Matrix()
+ {
+ var expected = Vector2.Transform(
+ new Vector2(1, 1),
+ Matrix3x2.CreateSkew(30, 20));
+
+ var matrix = Matrix.CreateSkew(30, 20);
+ var point = new Point(1, 1);
+ var actual = matrix.Transform(point);
+
+ AssertCoordinatesEqualWithReducedPrecision(expected, actual);
+ }
+}