Browse Source

Merge pull request #8184 from AvaloniaUI/transform-3d

3D transforms (updated to master)
pull/8188/head
Jumar Macato 4 years ago
committed by GitHub
parent
commit
d1afd3bb59
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  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>Slide</ComboBoxItem>
<ComboBoxItem>Crossfade</ComboBoxItem>
<ComboBoxItem>3D Rotation</ComboBoxItem>
</ComboBox>
</StackPanel>

3
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;
}
}
}

3
samples/RenderDemo/MainWindow.xaml

@ -72,5 +72,8 @@
<TabItem Header="Brushes">
<pages:BrushesPage />
</TabItem>
<TabItem Header="3D Transformation">
<pages:Transform3DPage />
</TabItem>
</controls:HamburgerMenu>
</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 RotateTransform());
normalTransform.Children.Add(new TranslateTransform());
normalTransform.Children.Add(new Rotate3DTransform());
ctrl.RenderTransform = normalTransform;
}

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

@ -10,7 +10,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Animation
{
/// <summary>
/// Transitions between two pages by sliding them horizontally.
/// Transitions between two pages by sliding them horizontally or vertically.
/// </summary>
public class PageSlide : IPageTransition
{
@ -62,7 +62,7 @@ namespace Avalonia.Animation
public Easing SlideOutEasing { get; set; } = new LinearEasing();
/// <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)
{
@ -157,7 +157,7 @@ namespace Avalonia.Animation
/// <remarks>
/// Any one of the parameters may be null, but not both.
/// </remarks>
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;

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.Globalization;
using System.Linq;
using System.Numerics;
using Avalonia.Utilities;
namespace Avalonia
{
/// <summary>
/// A 2x3 matrix.
/// A 3x3 matrix.
/// </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
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;
/// <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>
/// Initializes a new instance of the <see cref="Matrix"/> struct.
/// </summary>
/// <param name="m11">The first element of the first row.</param>
/// <param name="m12">The second element of the first row.</param>
/// <param name="m21">The first element of the second row.</param>
/// <param name="m22">The second element of the second row.</param>
/// <param name="scaleX">The first element of the first row.</param>
/// <param name="skrewY">The second element of the first row.</param>
/// <param name="persX">The third 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="persY">The third 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>
/// <param name="persZ">The third element of the third row.</param>
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;
}
/// <summary>
/// Returns the multiplicative identity matrix.
/// </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>
/// Returns whether the matrix is the identity matrix.
@ -60,35 +106,50 @@ namespace Avalonia
public bool HasInverse => !MathUtilities.IsZero(GetDeterminant());
/// <summary>
/// The first element of the first row
/// The first element of the first row (scaleX).
/// </summary>
public double M11 => _m11;
/// <summary>
/// The second element of the first row
/// The second element of the first row (skrewY).
/// </summary>
public double M12 => _m12;
/// <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>
public double M21 => _m21;
/// <summary>
/// The second element of the second row
/// The second element of the second row (scaleY).
/// </summary>
public double M22 => _m22;
/// <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>
public double M31 => _m31;
/// <summary>
/// The second element of the third row
/// The second element of the third row (offsetY/translateY).
/// </summary>
public double M32 => _m32;
/// <summary>
/// The third element of the third row (persZ: perspective scale factor).
/// </summary>
public double M33 => _m33;
/// <summary>
/// Multiplies two matrices together and returns the resulting matrix.
/// </summary>
@ -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));
}
/// <summary>
@ -171,7 +235,7 @@ namespace Avalonia
/// <returns>A scaling matrix.</returns>
public static Matrix CreateScale(double xScale, double yScale)
{
return CreateScale(new Vector(xScale, yScale));
return new Matrix(xScale, 0, 0, yScale, 0, 0);
}
/// <summary>
@ -181,7 +245,7 @@ namespace Avalonia
/// <returns>A scaling matrix.</returns>
public static Matrix CreateScale(Vector scales)
{
return new Matrix(scales.X, 0, 0, scales.Y, 0, 0);
return CreateScale(scales.X, scales.Y);
}
/// <summary>
@ -214,7 +278,7 @@ namespace Avalonia
{
return angle * 0.0174532925;
}
/// <summary>
/// Appends another matrix as post-multiplication operation.
/// Equivalent to this * value;
@ -227,7 +291,7 @@ namespace Avalonia
}
/// <summary>
/// Prpends another matrix as pre-multiplication operation.
/// Prepends another matrix as pre-multiplication operation.
/// Equivalent to value * this;
/// </summary>
/// <param name="value">A matrix.</param>
@ -247,7 +311,49 @@ namespace Avalonia
/// </remarks>
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>
@ -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
/// <returns>The hash code.</returns>
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();
}
/// <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>
@ -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());
}
/// <summary>
@ -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
/// <returns>The inverted matrix.</returns>
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
/// <summary>
/// Parses a <see cref="Matrix"/> string.
/// </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>
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
/// </summary>
/// <param name="matrix">Matrix to decompose.</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)
{
decomposed = default;
var determinant = matrix.GetDeterminant();
if (MathUtilities.IsZero(determinant))
if (MathUtilities.IsZero(determinant) || matrix.ContainsPerspective())
{
return false;
}

21
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
/// <param name="point">The point.</param>
/// <param name="matrix">The matrix.</param>
/// <returns>The resulting point.</returns>
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);
/// <summary>
/// Parses a <see cref="Point"/> string.
@ -242,18 +238,7 @@ namespace Avalonia
/// </summary>
/// <param name="transform">The transform.</param>
/// <returns>The transformed point.</returns>
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);
/// <summary>
/// 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(AvaloniaPropertyChangedEventArgs 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" />
<PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
</ItemGroup>
</Project>

6
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;

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