Browse Source

Merge branch 'Transformation3D' of https://github.com/yankun/Avalonia into transform-3d

# Conflicts:
#	samples/RenderDemo/MainWindow.xaml
#	src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
#	src/Avalonia.Base/Rotate3DTransform.cs
#	tests/Avalonia.Base.UnitTests/MatrixTests.cs
pull/8184/head
Nikita Tsukanov 4 years ago
parent
commit
eb7375d8a8
  1. 1
      samples/ControlCatalog/Pages/CarouselPage.xaml
  2. 3
      samples/ControlCatalog/Pages/CarouselPage.xaml.cs
  3. 3
      samples/RenderDemo/MainWindow.xaml
  4. 210
      samples/RenderDemo/Pages/Transform3DPage.axaml
  5. 21
      samples/RenderDemo/Pages/Transform3DPage.axaml.cs
  6. 55
      samples/RenderDemo/ViewModels/Transform3DPageViewModel.cs
  7. 1
      src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
  8. 6
      src/Avalonia.Base/Animation/PageSlide.cs
  9. 120
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  10. 268
      src/Avalonia.Base/Matrix.cs
  11. 21
      src/Avalonia.Base/Point.cs
  12. 210
      src/Avalonia.Base/Rotate3DTransform.cs
  13. 1
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  14. 6
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  15. 92
      tests/Avalonia.Base.UnitTests/MatrixTests.cs

1
samples/ControlCatalog/Pages/CarouselPage.xaml

@ -29,6 +29,7 @@
<ComboBoxItem>None</ComboBoxItem> <ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Slide</ComboBoxItem> <ComboBoxItem>Slide</ComboBoxItem>
<ComboBoxItem>Crossfade</ComboBoxItem> <ComboBoxItem>Crossfade</ComboBoxItem>
<ComboBoxItem>3D Rotation</ComboBoxItem>
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>

3
samples/ControlCatalog/Pages/CarouselPage.xaml.cs

@ -45,6 +45,9 @@ namespace ControlCatalog.Pages
case 2: case 2:
_carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25)); _carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25));
break; break;
case 3:
_carousel.PageTransition = new Rotate3DTransition(TimeSpan.FromSeconds(0.5), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
break;
} }
} }
} }

3
samples/RenderDemo/MainWindow.xaml

@ -72,5 +72,8 @@
<TabItem Header="Brushes"> <TabItem Header="Brushes">
<pages:BrushesPage /> <pages:BrushesPage />
</TabItem> </TabItem>
<TabItem Header="3D Transformation">
<pages:Transform3DPage />
</TabItem>
</controls:HamburgerMenu> </controls:HamburgerMenu>
</Window> </Window>

210
samples/RenderDemo/Pages/Transform3DPage.axaml

@ -0,0 +1,210 @@
<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="600" d:DesignHeight="700"
x:Class="RenderDemo.Pages.Transform3DPage">
<UserControl.Styles>
<Styles>
<Styles.Resources>
<Template x:Key="TestContent">
<Grid RowDefinitions="*,*" ColumnDefinitions="*,*" Margin="5">
<TextBlock>I'm a text</TextBlock>
<Button Grid.Row="0" Grid.Column="1" Content="A Button"></Button>
<Slider Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Value="{Binding Depth}"
Minimum="100"
Maximum="300" />
</Grid>
</Template>
</Styles.Resources>
</Styles>
<Style Selector="Border.Test">
<Setter Property="Width" Value="200" />
<Setter Property="Height" Value="200" />
<Setter Property="Child" Value="{StaticResource TestContent}" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="Grid.ColumnSpan" Value="2" />
</Style>
<Style Selector="TextBlock, Label, Slider">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="10,0,10,0" />
</Style>
<Style Selector="Border TextBlock">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Border Button">
<Setter Property="Background" Value="White"></Setter>
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="Border#B1">
<Style.Animations>
<Animation Duration="0:0:10"
IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Rotate3DTransform.AngleX" Value="0" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
<KeyFrame Cue="25%">
<Setter Property="Rotate3DTransform.AngleX" Value="90" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Rotate3DTransform.AngleX" Value="360" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border#B2">
<Style.Animations>
<Animation Duration="0:0:10"
IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Rotate3DTransform.AngleX" Value="90" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="25%">
<Setter Property="Rotate3DTransform.AngleX" Value="180" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="75%">
<Setter Property="Rotate3DTransform.AngleX" Value="360" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Rotate3DTransform.AngleX" Value="450" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border#B3">
<Style.Animations>
<Animation Duration="0:0:10"
IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Rotate3DTransform.AngleX" Value="180" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="Rotate3DTransform.AngleX" Value="360" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
<KeyFrame Cue="75%">
<Setter Property="Rotate3DTransform.AngleX" Value="450" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Rotate3DTransform.AngleX" Value="540" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border#B4">
<Style.Animations>
<Animation Duration="0:0:10"
IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="Rotate3DTransform.AngleX" Value="270" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="25%">
<Setter Property="Rotate3DTransform.AngleX" Value="360" />
<Setter Property="ZIndex" Value="4" />
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="Rotate3DTransform.AngleX" Value="450" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Rotate3DTransform.AngleX" Value="630" />
<Setter Property="ZIndex" Value="1" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="*, Auto, Auto, Auto, Auto, Auto, Auto, Auto">
<Grid.Clock>
<Clock />
</Grid.Clock>
<Border Name="B1" Background="DarkRed" Classes="Test">
<Border.RenderTransform>
<Rotate3DTransform CenterZ="-100"
Depth="{Binding Depth}" />
</Border.RenderTransform>
</Border>
<Border Name="B2" Grid.Row="0" Grid.Column="0" Classes="Test" Background="DarkGreen">
<Border.RenderTransform>
<Rotate3DTransform CenterZ="-100"
Depth="{Binding Depth}" />
</Border.RenderTransform>
</Border>
<Border Name="B3" Grid.Row="0" Grid.Column="0" Classes="Test" Background="DarkBlue">
<Border.RenderTransform>
<Rotate3DTransform CenterZ="-100"
Depth="{Binding Depth}" />
</Border.RenderTransform>
</Border>
<Border Name="B4" Grid.Row="0" Grid.Column="0" Classes="Test" Background="Orange">
<Border.RenderTransform>
<Rotate3DTransform CenterZ="-100"
Depth="{Binding Depth}" />
</Border.RenderTransform>
</Border>
<Label Grid.Column="0" Grid.Row="1">Depth: </Label>
<Slider Grid.Column="1" Grid.Row="1" Value="{Binding Depth}" Minimum="100" Maximum="300" />
<Border Grid.Row="0" Grid.Column="2" Classes="Test" ZIndex="-2">
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="Red" />
<GradientStop Offset="1" Color="Blue" />
</LinearGradientBrush>
</Border.Background>
<Border.Styles>
<Style Selector="Label">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Slider">
<Setter Property="Width" Value="100" />
</Style>
</Border.Styles>
<Border.RenderTransform>
<Rotate3DTransform Depth="{Binding Depth}"
CenterX="{Binding CenterX}"
CenterY="{Binding CenterY}"
CenterZ="{Binding CenterZ}"
AngleX="{Binding AngleX}"
AngleY="{Binding AngleY}"
AngleZ="{Binding AngleZ}" />
</Border.RenderTransform>
</Border>
<Label Grid.Row="1" Grid.Column="2">Center X: </Label>
<Slider Grid.Row="1" Grid.Column="3" Value="{Binding CenterX}" Minimum="-100" Maximum="100" />
<Label Grid.Row="2" Grid.Column="2">Center Y: </Label>
<Slider Grid.Row="2" Grid.Column="3" Value="{Binding CenterY}" Minimum="-100" Maximum="100" />
<Label Grid.Row="3" Grid.Column="2">Center Z: </Label>
<Slider Grid.Row="3" Grid.Column="3" Value="{Binding CenterZ}" Minimum="-100" Maximum="100" />
<Label Grid.Row="4" Grid.Column="2">Angle X: </Label>
<Slider Grid.Row="4" Grid.Column="3" Value="{Binding AngleX}" Minimum="-180" Maximum="180" />
<Label Grid.Row="5" Grid.Column="2">Angle Y: </Label>
<Slider Grid.Row="5" Grid.Column="3" Value="{Binding AngleY}" Minimum="-180" Maximum="180" />
<Label Grid.Row="6" Grid.Column="2">Angle Z: </Label>
<Slider Grid.Row="6" Grid.Column="3" Value="{Binding AngleZ}" Minimum="-180" Maximum="180" />
</Grid>
</UserControl>

21
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);
}
}

55
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);
}
}
}

1
src/Avalonia.Base/Animation/Animators/TransformAnimator.cs

@ -43,6 +43,7 @@ namespace Avalonia.Animation.Animators
normalTransform.Children.Add(new SkewTransform()); normalTransform.Children.Add(new SkewTransform());
normalTransform.Children.Add(new RotateTransform()); normalTransform.Children.Add(new RotateTransform());
normalTransform.Children.Add(new TranslateTransform()); normalTransform.Children.Add(new TranslateTransform());
normalTransform.Children.Add(new Rotate3DTransform());
ctrl.RenderTransform = normalTransform; ctrl.RenderTransform = normalTransform;
} }

6
src/Avalonia.Base/Animation/PageSlide.cs

@ -10,7 +10,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
/// <summary> /// <summary>
/// Transitions between two pages by sliding them horizontally. /// Transitions between two pages by sliding them horizontally or vertically.
/// </summary> /// </summary>
public class PageSlide : IPageTransition public class PageSlide : IPageTransition
{ {
@ -62,7 +62,7 @@ namespace Avalonia.Animation
public Easing SlideOutEasing { get; set; } = new LinearEasing(); public Easing SlideOutEasing { get; set; } = new LinearEasing();
/// <inheritdoc /> /// <inheritdoc />
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) if (cancellationToken.IsCancellationRequested)
{ {
@ -157,7 +157,7 @@ namespace Avalonia.Animation
/// <remarks> /// <remarks>
/// Any one of the parameters may be null, but not both. /// Any one of the parameters may be null, but not both.
/// </remarks> /// </remarks>
private static IVisual GetVisualParent(IVisual? from, IVisual? to) protected static IVisual GetVisualParent(IVisual? from, IVisual? to)
{ {
var p1 = (from ?? to)!.VisualParent; var p1 = (from ?? to)!.VisualParent;
var p2 = (to ?? from)!.VisualParent; var p2 = (to ?? from)!.VisualParent;

120
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
{
/// <summary>
/// Creates a new instance of the <see cref="Rotate3DTransition"/>
/// </summary>
/// <param name="duration">How long the rotation should take place</param>
/// <param name="orientation">The orientation of the rotation</param>
public Rotate3DTransition(TimeSpan duration, SlideAxis orientation = SlideAxis.Horizontal, double? depth = null)
: base(duration, orientation)
{
Depth = depth;
}
/// <summary>
/// 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.
/// </summary>
public double? Depth { get; set; }
/// <summary>
/// Creates a new instance of the <see cref="Rotate3DTransition"/>
/// </summary>
public Rotate3DTransition() { }
/// <inheritdoc />
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;
}
}
}
}

268
src/Avalonia.Base/Matrix.cs

@ -1,12 +1,22 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Numerics;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
{ {
/// <summary> /// <summary>
/// A 2x3 matrix. /// A 3x3 matrix.
/// </summary> /// </summary>
/// <remakrs>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).
/// </remakrs>
#if !BUILDTASK #if !BUILDTASK
public public
#endif #endif
@ -14,40 +24,76 @@ namespace Avalonia
{ {
private readonly double _m11; private readonly double _m11;
private readonly double _m12; private readonly double _m12;
private readonly double _m13;
private readonly double _m21; private readonly double _m21;
private readonly double _m22; private readonly double _m22;
private readonly double _m23;
private readonly double _m31; private readonly double _m31;
private readonly double _m32; private readonly double _m32;
private readonly double _m33;
/// <summary>
/// Initializes a new instance of the <see cref="Matrix"/> struct (equivalent to a 2x3 Matrix without perspective).
/// </summary>
/// <param name="scaleX">The first element of the first row.</param>
/// <param name="skrewY">The second element of the first row.</param>
/// <param name="skrewX">The first element of the second row.</param>
/// <param name="scaleY">The second element of the second row.</param>
/// <param name="offsetX">The first element of the third row.</param>
/// <param name="offsetY">The second element of the third row.</param>
public Matrix(
double scaleX,
double skrewY,
double skrewX,
double scaleY,
double offsetX,
double offsetY) : this( scaleX, skrewY, 0, skrewX, scaleY, 0, offsetX, offsetY, 1)
{
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Matrix"/> struct. /// Initializes a new instance of the <see cref="Matrix"/> struct.
/// </summary> /// </summary>
/// <param name="m11">The first element of the first row.</param> /// <param name="scaleX">The first element of the first row.</param>
/// <param name="m12">The second element of the first row.</param> /// <param name="skrewY">The second element of the first row.</param>
/// <param name="m21">The first element of the second row.</param> /// <param name="persX">The third element of the first row.</param>
/// <param name="m22">The second element of the second row.</param> /// <param name="skrewX">The first element of the second row.</param>
/// <param name="scaleY">The second element of the second row.</param>
/// <param name="persY">The third element of the second row.</param>
/// <param name="offsetX">The first element of the third row.</param> /// <param name="offsetX">The first element of the third row.</param>
/// <param name="offsetY">The second element of the third row.</param> /// <param name="offsetY">The second element of the third row.</param>
/// <param name="persZ">The third element of the third row.</param>
public Matrix( public Matrix(
double m11, double scaleX,
double m12, double skrewY,
double m21, double persX,
double m22, double skrewX,
double scaleY,
double persY,
double offsetX, double offsetX,
double offsetY) double offsetY,
double persZ)
{ {
_m11 = m11; _m11 = scaleX;
_m12 = m12; _m12 = skrewY;
_m21 = m21; _m13 = persX;
_m22 = m22; _m21 = skrewX;
_m22 = scaleY;
_m23 = persY;
_m31 = offsetX; _m31 = offsetX;
_m32 = offsetY; _m32 = offsetY;
_m33 = persZ;
} }
/// <summary> /// <summary>
/// Returns the multiplicative identity matrix. /// Returns the multiplicative identity matrix.
/// </summary> /// </summary>
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);
/// <summary> /// <summary>
/// Returns whether the matrix is the identity matrix. /// Returns whether the matrix is the identity matrix.
@ -60,35 +106,50 @@ namespace Avalonia
public bool HasInverse => !MathUtilities.IsZero(GetDeterminant()); public bool HasInverse => !MathUtilities.IsZero(GetDeterminant());
/// <summary> /// <summary>
/// The first element of the first row /// The first element of the first row (scaleX).
/// </summary> /// </summary>
public double M11 => _m11; public double M11 => _m11;
/// <summary> /// <summary>
/// The second element of the first row /// The second element of the first row (skrewY).
/// </summary> /// </summary>
public double M12 => _m12; public double M12 => _m12;
/// <summary> /// <summary>
/// The first element of the second row /// The third element of the first row (persX: input x-axis perspective factor).
/// </summary>
public double M13 => _m13;
/// <summary>
/// The first element of the second row (skrewX).
/// </summary> /// </summary>
public double M21 => _m21; public double M21 => _m21;
/// <summary> /// <summary>
/// The second element of the second row /// The second element of the second row (scaleY).
/// </summary> /// </summary>
public double M22 => _m22; public double M22 => _m22;
/// <summary> /// <summary>
/// The first element of the third row /// The third element of the second row (persY: input y-axis perspective factor).
/// </summary>
public double M23 => _m23;
/// <summary>
/// The first element of the third row (offsetX/translateX).
/// </summary> /// </summary>
public double M31 => _m31; public double M31 => _m31;
/// <summary> /// <summary>
/// The second element of the third row /// The second element of the third row (offsetY/translateY).
/// </summary> /// </summary>
public double M32 => _m32; public double M32 => _m32;
/// <summary>
/// The third element of the third row (persZ: perspective scale factor).
/// </summary>
public double M33 => _m33;
/// <summary> /// <summary>
/// Multiplies two matrices together and returns the resulting matrix. /// Multiplies two matrices together and returns the resulting matrix.
/// </summary> /// </summary>
@ -98,12 +159,15 @@ namespace Avalonia
public static Matrix operator *(Matrix value1, Matrix value2) public static Matrix operator *(Matrix value1, Matrix value2)
{ {
return new Matrix( return new Matrix(
(value1.M11 * value2.M11) + (value1.M12 * value2.M21), (value1.M11 * value2.M11) + (value1.M12 * value2.M21) + (value1.M13 * value2.M31),
(value1.M11 * value2.M12) + (value1.M12 * value2.M22), (value1.M11 * value2.M12) + (value1.M12 * value2.M22) + (value1.M13 * value2.M32),
(value1.M21 * value2.M11) + (value1.M22 * value2.M21), (value1.M11 * value2.M13) + (value1.M12 * value2.M23) + (value1.M13 * value2.M33),
(value1.M21 * value2.M12) + (value1.M22 * value2.M22), (value1.M21 * value2.M11) + (value1.M22 * value2.M21) + (value1.M23 * value2.M31),
(value1._m31 * value2.M11) + (value1._m32 * value2.M21) + value2._m31, (value1.M21 * value2.M12) + (value1.M22 * value2.M22) + (value1.M23 * value2.M32),
(value1._m31 * value2.M12) + (value1._m32 * value2.M22) + 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));
} }
/// <summary> /// <summary>
@ -171,7 +235,7 @@ namespace Avalonia
/// <returns>A scaling matrix.</returns> /// <returns>A scaling matrix.</returns>
public static Matrix CreateScale(double xScale, double yScale) public static Matrix CreateScale(double xScale, double yScale)
{ {
return CreateScale(new Vector(xScale, yScale)); return new Matrix(xScale, 0, 0, yScale, 0, 0);
} }
/// <summary> /// <summary>
@ -181,7 +245,7 @@ namespace Avalonia
/// <returns>A scaling matrix.</returns> /// <returns>A scaling matrix.</returns>
public static Matrix CreateScale(Vector scales) public static Matrix CreateScale(Vector scales)
{ {
return new Matrix(scales.X, 0, 0, scales.Y, 0, 0); return CreateScale(scales.X, scales.Y);
} }
/// <summary> /// <summary>
@ -214,7 +278,7 @@ namespace Avalonia
{ {
return angle * 0.0174532925; return angle * 0.0174532925;
} }
/// <summary> /// <summary>
/// Appends another matrix as post-multiplication operation. /// Appends another matrix as post-multiplication operation.
/// Equivalent to this * value; /// Equivalent to this * value;
@ -227,7 +291,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Prpends another matrix as pre-multiplication operation. /// Prepends another matrix as pre-multiplication operation.
/// Equivalent to value * this; /// Equivalent to value * this;
/// </summary> /// </summary>
/// <param name="value">A matrix.</param> /// <param name="value">A matrix.</param>
@ -247,7 +311,49 @@ namespace Avalonia
/// </remarks> /// </remarks>
public double GetDeterminant() 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);
}
/// <summary>
/// Transforms the point with the matrix
/// </summary>
/// <param name="p">The point to be transformed</param>
/// <returns>The transformed point</returns>
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;
} }
/// <summary> /// <summary>
@ -260,10 +366,13 @@ namespace Avalonia
// ReSharper disable CompareOfFloatsByEqualityOperator // ReSharper disable CompareOfFloatsByEqualityOperator
return _m11 == other.M11 && return _m11 == other.M11 &&
_m12 == other.M12 && _m12 == other.M12 &&
_m13 == other.M13 &&
_m21 == other.M21 && _m21 == other.M21 &&
_m22 == other.M22 && _m22 == other.M22 &&
_m23 == other.M23 &&
_m31 == other.M31 && _m31 == other.M31 &&
_m32 == other.M32; _m32 == other.M32 &&
_m33 == other.M33;
// ReSharper restore CompareOfFloatsByEqualityOperator // ReSharper restore CompareOfFloatsByEqualityOperator
} }
@ -280,9 +389,18 @@ namespace Avalonia
/// <returns>The hash code.</returns> /// <returns>The hash code.</returns>
public override int GetHashCode() public override int GetHashCode()
{ {
return M11.GetHashCode() + M12.GetHashCode() + return (_m11, _m12, _m13, _m21, _m22, _m23, _m31, _m32, _m33).GetHashCode();
M21.GetHashCode() + M22.GetHashCode() + }
M31.GetHashCode() + M32.GetHashCode();
/// <summary>
/// Determines if the current matrix contains perspective (non-affine) transforms (true) or only (affine) transforms that could be mapped into an 2x3 matrix (false).
/// </summary>
public bool ContainsPerspective()
{
// ReSharper disable CompareOfFloatsByEqualityOperator
return _m13 != 0 || _m23 != 0 || _m33 != 1;
// ReSharper restore CompareOfFloatsByEqualityOperator
} }
/// <summary> /// <summary>
@ -292,15 +410,25 @@ namespace Avalonia
public override string ToString() public override string ToString()
{ {
CultureInfo ci = CultureInfo.CurrentCulture; 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( return string.Format(
ci, ci,
"{{ {{M11:{0} M12:{1}}} {{M21:{2} M22:{3}}} {{M31:{4} M32:{5}}} }}", msg,
M11.ToString(ci), values.Select((v) => v.ToString(ci)).ToArray());
M12.ToString(ci),
M21.ToString(ci),
M22.ToString(ci),
M31.ToString(ci),
M32.ToString(ci));
} }
/// <summary> /// <summary>
@ -318,14 +446,20 @@ namespace Avalonia
return false; return false;
} }
var invdet = 1 / d;
inverted = new Matrix( inverted = new Matrix(
_m22 / d, (_m22 * _m33 - _m32 * _m23) * invdet,
-_m12 / d, (_m13 * _m31 - _m12 * _m33) * invdet,
-_m21 / d, (_m12 * _m23 - _m13 * _m22) * invdet,
_m11 / d, (_m23 * _m31 - _m21 * _m33) * invdet,
((_m21 * _m32) - (_m22 * _m31)) / d, (_m11 * _m33 - _m13 * _m31) * invdet,
((_m12 * _m31) - (_m11 * _m32)) / d); (_m21 * _m13 - _m11 * _m23) * invdet,
(_m21 * _m32 - _m31 * _m22) * invdet,
(_m21 * _m12 - _m11 * _m32) * invdet,
(_m11 * _m22 - _m21 * _m12) * invdet
);
return true; return true;
} }
@ -336,7 +470,7 @@ namespace Avalonia
/// <returns>The inverted matrix.</returns> /// <returns>The inverted matrix.</returns>
public Matrix Invert() public Matrix Invert()
{ {
if (!TryInvert(out Matrix inverted)) if (!TryInvert(out var inverted))
{ {
throw new InvalidOperationException("Transform is not invertible."); throw new InvalidOperationException("Transform is not invertible.");
} }
@ -347,20 +481,30 @@ namespace Avalonia
/// <summary> /// <summary>
/// Parses a <see cref="Matrix"/> string. /// Parses a <see cref="Matrix"/> string.
/// </summary> /// </summary>
/// <param name="s">Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new <see cref="Matrix"/></param> /// <param name="s">Six or nine comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY[, persX, persY, persZ]) that describe the new <see cref="Matrix"/></param>
/// <returns>The <see cref="Matrix"/>.</returns> /// <returns>The <see cref="Matrix"/>.</returns>
public static Matrix Parse(string s) 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.")) using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix."))
{ {
return new Matrix( var v1 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v2 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v3 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v4 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v5 = tokenizer.ReadDouble();
tokenizer.ReadDouble(), var v6 = tokenizer.ReadDouble();
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
/// </summary> /// </summary>
/// <param name="matrix">Matrix to decompose.</param> /// <param name="matrix">Matrix to decompose.</param>
/// <param name="decomposed">Decomposed matrix.</param> /// <param name="decomposed">Decomposed matrix.</param>
/// <returns>The status of the operation.</returns> /// <returns>The status of the operation.</returns>
public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed) public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed)
{ {
decomposed = default; decomposed = default;
var determinant = matrix.GetDeterminant(); var determinant = matrix.GetDeterminant();
if (MathUtilities.IsZero(determinant)) if (MathUtilities.IsZero(determinant) || matrix.ContainsPerspective())
{ {
return false; return false;
} }

21
src/Avalonia.Base/Point.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Numerics;
#if !BUILDTASK #if !BUILDTASK
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
#endif #endif
@ -168,12 +169,7 @@ namespace Avalonia
/// <param name="point">The point.</param> /// <param name="point">The point.</param>
/// <param name="matrix">The matrix.</param> /// <param name="matrix">The matrix.</param>
/// <returns>The resulting point.</returns> /// <returns>The resulting point.</returns>
public static Point operator *(Point point, Matrix matrix) public static Point operator *(Point point, Matrix matrix) => matrix.Transform(point);
{
return new Point(
(point.X * matrix.M11) + (point.Y * matrix.M21) + matrix.M31,
(point.X * matrix.M12) + (point.Y * matrix.M22) + matrix.M32);
}
/// <summary> /// <summary>
/// Parses a <see cref="Point"/> string. /// Parses a <see cref="Point"/> string.
@ -242,18 +238,7 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="transform">The transform.</param> /// <param name="transform">The transform.</param>
/// <returns>The transformed point.</returns> /// <returns>The transformed point.</returns>
public Point Transform(Matrix transform) public Point Transform(Matrix transform) => transform.Transform(this);
{
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);
}
/// <summary> /// <summary>
/// Returns a new point with the specified X coordinate. /// Returns a new point with the specified X coordinate.

210
src/Avalonia.Base/Rotate3DTransform.cs

@ -0,0 +1,210 @@
using System;
using System.Numerics;
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
/// <summary>
/// Non-Affine 3D transformation for rotating a visual around a definable axis
/// </summary>
public class Rotate3DTransform : Transform
{
private readonly bool _isInitializing;
/// <summary>
/// Defines the <see cref="AngleX"/> property.
/// </summary>
public static readonly StyledProperty<double> AngleXProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(AngleX));
/// <summary>
/// Defines the <see cref="AngleY"/> property.
/// </summary>
public static readonly StyledProperty<double> AngleYProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(AngleY));
/// <summary>
/// Defines the <see cref="AngleZ"/> property.
/// </summary>
public static readonly StyledProperty<double> AngleZProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(AngleZ));
/// <summary>
/// Defines the <see cref="CenterX"/> property.
/// </summary>
public static readonly StyledProperty<double> CenterXProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(CenterX));
/// <summary>
/// Defines the <see cref="CenterY"/> property.
/// </summary>
public static readonly StyledProperty<double> CenterYProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(CenterY));
/// <summary>
/// Defines the <see cref="CenterZ"/> property.
/// </summary>
public static readonly StyledProperty<double> CenterZProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(CenterZ));
/// <summary>
/// Defines the <see cref="Depth"/> property.
/// </summary>
public static readonly StyledProperty<double> DepthProperty =
AvaloniaProperty.Register<Rotate3DTransform, double>(nameof(Depth));
/// <summary>
/// Initializes a new instance of the <see cref="Rotate3DTransform"/> class.
/// </summary>
public Rotate3DTransform() { }
/// <summary>
/// Initializes a new instance of the <see cref="Rotate3DTransform"/> class.
/// </summary>
/// <param name="angleX">The rotation around the X-Axis</param>
/// <param name="angleY">The rotation around the Y-Axis</param>
/// <param name="angleZ">The rotation around the Z-Axis</param>
/// <param name="centerX">The origin of the X-Axis</param>
/// <param name="centerY">The origin of the Y-Axis</param>
/// <param name="centerZ">The origin of the Z-Axis</param>
/// <param name="depth">The depth of the 3D effect</param>
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;
}
/// <summary>
/// Sets the rotation around the X-Axis
/// </summary>
public double AngleX
{
get => GetValue(AngleXProperty);
set => SetValue(AngleXProperty, value);
}
/// <summary>
/// Sets the rotation around the Y-Axis
/// </summary>
public double AngleY
{
get => GetValue(AngleYProperty);
set => SetValue(AngleYProperty, value);
}
/// <summary>
/// Sets the rotation around the Z-Axis
/// </summary>
public double AngleZ
{
get => GetValue(AngleZProperty);
set => SetValue(AngleZProperty, value);
}
/// <summary>
/// Moves the origin the X-Axis rotates around
/// </summary>
public double CenterX
{
get => GetValue(CenterXProperty);
set => SetValue(CenterXProperty, value);
}
/// <summary>
/// Moves the origin the Y-Axis rotates around
/// </summary>
public double CenterY
{
get => GetValue(CenterYProperty);
set => SetValue(CenterYProperty, value);
}
/// <summary>
/// Moves the origin the Z-Axis rotates around
/// </summary>
public double CenterZ
{
get => GetValue(CenterZProperty);
set => SetValue(CenterZProperty, value);
}
/// <summary>
/// Affects the depth of the rotation effect
/// </summary>
public double Depth
{
get => GetValue(DepthProperty);
set => SetValue(DepthProperty, value);
}
/// <summary>
/// Gets the transform's <see cref="Matrix"/>.
/// </summary>
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<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (!_isInitializing) RaiseChanged();
}
}

1
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -103,5 +103,6 @@
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" /> <Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" />
<PackageReference Include="Mono.Cecil" Version="0.11.2" /> <PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" /> <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

6
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -103,9 +103,9 @@ namespace Avalonia.Skia
SkewY = (float)m.M12, SkewY = (float)m.M12,
ScaleY = (float)m.M22, ScaleY = (float)m.M22,
TransY = (float)m.M32, TransY = (float)m.M32,
Persp0 = 0, Persp0 = (float)m.M13,
Persp1 = 0, Persp1 = (float)m.M23,
Persp2 = 1 Persp2 = (float)m.M33
}; };
return sm; return sm;

92
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;
/// <summary>
/// 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.
/// </summary>
public class MatrixTests
{
/// <summary>
/// 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.
/// </summary>
/// <param name="expected">The expected vector</param>
/// <param name="actual">The actual transformed point</param>
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);
}
}
Loading…
Cancel
Save