19 changed files with 1353 additions and 15 deletions
@ -0,0 +1,35 @@ |
|||
using System; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Transformation; |
|||
|
|||
namespace Avalonia.Animation.Animators |
|||
{ |
|||
public class TransformOperationsAnimator : Animator<ITransform> |
|||
{ |
|||
public TransformOperationsAnimator() |
|||
{ |
|||
Validate = ValidateTransform; |
|||
} |
|||
|
|||
private void ValidateTransform(AnimatorKeyFrame kf) |
|||
{ |
|||
if (!(kf.Value is TransformOperations)) |
|||
{ |
|||
throw new InvalidOperationException($"All keyframes must be of type {typeof(TransformOperations)}."); |
|||
} |
|||
} |
|||
|
|||
public override ITransform Interpolate(double progress, ITransform oldValue, ITransform newValue) |
|||
{ |
|||
var oldTransform = Cast(oldValue); |
|||
var newTransform = Cast(newValue); |
|||
|
|||
return TransformOperations.Interpolate(oldTransform, newTransform, progress); |
|||
} |
|||
|
|||
private static TransformOperations Cast(ITransform value) |
|||
{ |
|||
return value as TransformOperations ?? TransformOperations.Identity; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.Animation.Animators; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Animation |
|||
{ |
|||
public class TransformOperationsTransition : Transition<ITransform> |
|||
{ |
|||
private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator(); |
|||
|
|||
public override IObservable<ITransform> DoTransition(IObservable<double> progress, |
|||
ITransform oldValue, |
|||
ITransform newValue) |
|||
{ |
|||
return progress |
|||
.Select(p => |
|||
{ |
|||
var f = Easing.Ease(p); |
|||
|
|||
return _operationsAnimator.Interpolate(f, oldValue, newValue); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
public interface IMutableTransform : ITransform |
|||
{ |
|||
/// <summary>
|
|||
/// Raised when the transform changes.
|
|||
/// </summary>
|
|||
event EventHandler Changed; |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System.ComponentModel; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
[TypeConverter(typeof(TransformConverter))] |
|||
public interface ITransform |
|||
{ |
|||
Matrix Value { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
using Avalonia.Media.Transformation; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Creates an <see cref="ITransform"/> from a string representation.
|
|||
/// </summary>
|
|||
public class TransformConverter : TypeConverter |
|||
{ |
|||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|||
{ |
|||
return sourceType == typeof(string); |
|||
} |
|||
|
|||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|||
{ |
|||
return TransformOperations.Parse((string)value); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
namespace Avalonia.Media.Transformation |
|||
{ |
|||
internal static class InterpolationUtilities |
|||
{ |
|||
public static double InterpolateScalars(double from, double to, double progress) |
|||
{ |
|||
return from * (1d - progress) + to * progress; |
|||
} |
|||
|
|||
public static Vector InterpolateVectors(Vector from, Vector to, double progress) |
|||
{ |
|||
var x = InterpolateScalars(from.X, to.X, progress); |
|||
var y = InterpolateScalars(from.Y, to.Y, progress); |
|||
|
|||
return new Vector(x, y); |
|||
} |
|||
|
|||
public static Matrix ComposeTransform(Matrix.Decomposed decomposed) |
|||
{ |
|||
// According to https://www.w3.org/TR/css-transforms-1/#recomposing-to-a-2d-matrix
|
|||
|
|||
return Matrix.CreateTranslation(decomposed.Translate) * |
|||
Matrix.CreateRotation(decomposed.Angle) * |
|||
Matrix.CreateSkew(decomposed.Skew.X, decomposed.Skew.Y) * |
|||
Matrix.CreateScale(decomposed.Scale); |
|||
} |
|||
|
|||
public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progres) |
|||
{ |
|||
Matrix.Decomposed result = default; |
|||
|
|||
result.Translate = InterpolateVectors(from.Translate, to.Translate, progres); |
|||
result.Scale = InterpolateVectors(from.Scale, to.Scale, progres); |
|||
result.Skew = InterpolateVectors(from.Skew, to.Skew, progres); |
|||
result.Angle = InterpolateScalars(from.Angle, to.Angle, progres); |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,203 @@ |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace Avalonia.Media.Transformation |
|||
{ |
|||
public struct TransformOperation |
|||
{ |
|||
public OperationType Type; |
|||
public Matrix Matrix; |
|||
public DataLayout Data; |
|||
|
|||
public enum OperationType |
|||
{ |
|||
Translate, |
|||
Rotate, |
|||
Scale, |
|||
Skew, |
|||
Matrix, |
|||
Identity |
|||
} |
|||
|
|||
public bool IsIdentity => Matrix.IsIdentity; |
|||
|
|||
public void Bake() |
|||
{ |
|||
Matrix = Matrix.Identity; |
|||
|
|||
switch (Type) |
|||
{ |
|||
case OperationType.Translate: |
|||
{ |
|||
Matrix = Matrix.CreateTranslation(Data.Translate.X, Data.Translate.Y); |
|||
|
|||
break; |
|||
} |
|||
case OperationType.Rotate: |
|||
{ |
|||
Matrix = Matrix.CreateRotation(Data.Rotate.Angle); |
|||
|
|||
break; |
|||
} |
|||
case OperationType.Scale: |
|||
{ |
|||
Matrix = Matrix.CreateScale(Data.Scale.X, Data.Scale.Y); |
|||
|
|||
break; |
|||
} |
|||
case OperationType.Skew: |
|||
{ |
|||
Matrix = Matrix.CreateSkew(Data.Skew.X, Data.Skew.Y); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static bool IsOperationIdentity(ref TransformOperation? operation) |
|||
{ |
|||
return !operation.HasValue || operation.Value.IsIdentity; |
|||
} |
|||
|
|||
public static bool TryInterpolate(TransformOperation? from, TransformOperation? to, double progress, |
|||
ref TransformOperation result) |
|||
{ |
|||
bool fromIdentity = IsOperationIdentity(ref from); |
|||
bool toIdentity = IsOperationIdentity(ref to); |
|||
|
|||
if (fromIdentity && toIdentity) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
TransformOperation fromValue = fromIdentity ? default : from.Value; |
|||
TransformOperation toValue = toIdentity ? default : to.Value; |
|||
|
|||
var interpolationType = toIdentity ? fromValue.Type : toValue.Type; |
|||
|
|||
result.Type = interpolationType; |
|||
|
|||
switch (interpolationType) |
|||
{ |
|||
case OperationType.Translate: |
|||
{ |
|||
double fromX = fromIdentity ? 0 : fromValue.Data.Translate.X; |
|||
double fromY = fromIdentity ? 0 : fromValue.Data.Translate.Y; |
|||
|
|||
double toX = toIdentity ? 0 : toValue.Data.Translate.X; |
|||
double toY = toIdentity ? 0 : toValue.Data.Translate.Y; |
|||
|
|||
result.Data.Translate.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); |
|||
result.Data.Translate.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); |
|||
|
|||
result.Bake(); |
|||
|
|||
break; |
|||
} |
|||
case OperationType.Rotate: |
|||
{ |
|||
double fromAngle = fromIdentity ? 0 : fromValue.Data.Rotate.Angle; |
|||
|
|||
double toAngle = toIdentity ? 0 : toValue.Data.Rotate.Angle; |
|||
|
|||
result.Data.Rotate.Angle = InterpolationUtilities.InterpolateScalars(fromAngle, toAngle, progress); |
|||
|
|||
result.Bake(); |
|||
|
|||
break; |
|||
} |
|||
case OperationType.Scale: |
|||
{ |
|||
double fromX = fromIdentity ? 1 : fromValue.Data.Scale.X; |
|||
double fromY = fromIdentity ? 1 : fromValue.Data.Scale.Y; |
|||
|
|||
double toX = toIdentity ? 1 : toValue.Data.Scale.X; |
|||
double toY = toIdentity ? 1 : toValue.Data.Scale.Y; |
|||
|
|||
result.Data.Scale.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); |
|||
result.Data.Scale.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); |
|||
|
|||
result.Bake(); |
|||
|
|||
break; |
|||
} |
|||
case OperationType.Skew: |
|||
{ |
|||
double fromX = fromIdentity ? 0 : fromValue.Data.Skew.X; |
|||
double fromY = fromIdentity ? 0 : fromValue.Data.Skew.Y; |
|||
|
|||
double toX = toIdentity ? 0 : toValue.Data.Skew.X; |
|||
double toY = toIdentity ? 0 : toValue.Data.Skew.Y; |
|||
|
|||
result.Data.Skew.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress); |
|||
result.Data.Skew.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress); |
|||
|
|||
result.Bake(); |
|||
|
|||
break; |
|||
} |
|||
case OperationType.Matrix: |
|||
{ |
|||
var fromMatrix = fromIdentity ? Matrix.Identity : fromValue.Matrix; |
|||
var toMatrix = toIdentity ? Matrix.Identity : toValue.Matrix; |
|||
|
|||
if (!Matrix.TryDecomposeTransform(fromMatrix, out Matrix.Decomposed fromDecomposed) || |
|||
!Matrix.TryDecomposeTransform(toMatrix, out Matrix.Decomposed toDecomposed)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var interpolated = |
|||
InterpolationUtilities.InterpolateDecomposedTransforms( |
|||
ref fromDecomposed, ref toDecomposed, |
|||
progress); |
|||
|
|||
result.Matrix = InterpolationUtilities.ComposeTransform(interpolated); |
|||
|
|||
break; |
|||
} |
|||
case OperationType.Identity: |
|||
{ |
|||
// Do nothing.
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
[StructLayout(LayoutKind.Explicit)] |
|||
public struct DataLayout |
|||
{ |
|||
[FieldOffset(0)] public SkewLayout Skew; |
|||
|
|||
[FieldOffset(0)] public ScaleLayout Scale; |
|||
|
|||
[FieldOffset(0)] public TranslateLayout Translate; |
|||
|
|||
[FieldOffset(0)] public RotateLayout Rotate; |
|||
|
|||
public struct SkewLayout |
|||
{ |
|||
public double X; |
|||
public double Y; |
|||
} |
|||
|
|||
public struct ScaleLayout |
|||
{ |
|||
public double X; |
|||
public double Y; |
|||
} |
|||
|
|||
public struct TranslateLayout |
|||
{ |
|||
public double X; |
|||
public double Y; |
|||
} |
|||
|
|||
public struct RotateLayout |
|||
{ |
|||
public double Angle; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,252 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Avalonia.Media.Transformation |
|||
{ |
|||
public sealed class TransformOperations : ITransform |
|||
{ |
|||
public static TransformOperations Identity { get; } = new TransformOperations(new List<TransformOperation>()); |
|||
|
|||
private readonly List<TransformOperation> _operations; |
|||
|
|||
private TransformOperations(List<TransformOperation> operations) |
|||
{ |
|||
_operations = operations ?? throw new ArgumentNullException(nameof(operations)); |
|||
|
|||
IsIdentity = CheckIsIdentity(); |
|||
|
|||
Value = ApplyTransforms(); |
|||
} |
|||
|
|||
public bool IsIdentity { get; } |
|||
|
|||
public IReadOnlyList<TransformOperation> Operations => _operations; |
|||
|
|||
public Matrix Value { get; } |
|||
|
|||
public static TransformOperations Parse(string s) |
|||
{ |
|||
return TransformParser.Parse(s); |
|||
} |
|||
|
|||
public static Builder CreateBuilder(int capacity) |
|||
{ |
|||
return new Builder(capacity); |
|||
} |
|||
|
|||
public static TransformOperations Interpolate(TransformOperations from, TransformOperations to, double progress) |
|||
{ |
|||
TransformOperations result = Identity; |
|||
|
|||
if (!TryInterpolate(from, to, progress, ref result)) |
|||
{ |
|||
// If the matrices cannot be interpolated, fallback to discrete animation logic.
|
|||
// See https://drafts.csswg.org/css-transforms/#matrix-interpolation
|
|||
result = progress < 0.5 ? from : to; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private Matrix ApplyTransforms(int startOffset = 0) |
|||
{ |
|||
Matrix matrix = Matrix.Identity; |
|||
|
|||
for (var i = startOffset; i < _operations.Count; i++) |
|||
{ |
|||
TransformOperation operation = _operations[i]; |
|||
matrix *= operation.Matrix; |
|||
} |
|||
|
|||
return matrix; |
|||
} |
|||
|
|||
private bool CheckIsIdentity() |
|||
{ |
|||
foreach (TransformOperation operation in _operations) |
|||
{ |
|||
if (!operation.IsIdentity) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private static bool TryInterpolate(TransformOperations from, TransformOperations to, double progress, ref TransformOperations result) |
|||
{ |
|||
bool fromIdentity = from.IsIdentity; |
|||
bool toIdentity = to.IsIdentity; |
|||
|
|||
if (fromIdentity && toIdentity) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
int matchingPrefixLength = ComputeMatchingPrefixLength(from, to); |
|||
int fromSize = fromIdentity ? 0 : from._operations.Count; |
|||
int toSize = toIdentity ? 0 : to._operations.Count; |
|||
int numOperations = Math.Max(fromSize, toSize); |
|||
|
|||
var builder = new Builder(matchingPrefixLength); |
|||
|
|||
for (int i = 0; i < matchingPrefixLength; i++) |
|||
{ |
|||
TransformOperation interpolated = new TransformOperation |
|||
{ |
|||
Type = TransformOperation.OperationType.Identity |
|||
}; |
|||
|
|||
if (!TransformOperation.TryInterpolate( |
|||
i >= fromSize ? default(TransformOperation?) : from._operations[i], |
|||
i >= toSize ? default(TransformOperation?) : to._operations[i], |
|||
progress, |
|||
ref interpolated)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
builder.Append(interpolated); |
|||
} |
|||
|
|||
if (matchingPrefixLength < numOperations) |
|||
{ |
|||
if (!ComputeDecomposedTransform(from, matchingPrefixLength, out Matrix.Decomposed fromDecomposed) || |
|||
!ComputeDecomposedTransform(to, matchingPrefixLength, out Matrix.Decomposed toDecomposed)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var transform = InterpolationUtilities.InterpolateDecomposedTransforms(ref fromDecomposed, ref toDecomposed, progress); |
|||
|
|||
builder.AppendMatrix(InterpolationUtilities.ComposeTransform(transform)); |
|||
} |
|||
|
|||
result = builder.Build(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private static bool ComputeDecomposedTransform(TransformOperations operations, int startOffset, out Matrix.Decomposed decomposed) |
|||
{ |
|||
Matrix transform = operations.ApplyTransforms(startOffset); |
|||
|
|||
if (!Matrix.TryDecomposeTransform(transform, out decomposed)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private static int ComputeMatchingPrefixLength(TransformOperations from, TransformOperations to) |
|||
{ |
|||
int numOperations = Math.Min(from._operations.Count, to._operations.Count); |
|||
|
|||
for (int i = 0; i < numOperations; i++) |
|||
{ |
|||
if (from._operations[i].Type != to._operations[i].Type) |
|||
{ |
|||
return i; |
|||
} |
|||
} |
|||
|
|||
// If the operations match to the length of the shorter list, then pad its
|
|||
// length with the matching identity operations.
|
|||
// https://drafts.csswg.org/css-transforms/#transform-function-lists
|
|||
return Math.Max(from._operations.Count, to._operations.Count); |
|||
} |
|||
|
|||
public readonly struct Builder |
|||
{ |
|||
private readonly List<TransformOperation> _operations; |
|||
|
|||
public Builder(int capacity) |
|||
{ |
|||
_operations = new List<TransformOperation>(capacity); |
|||
} |
|||
|
|||
public void AppendTranslate(double x, double y) |
|||
{ |
|||
var toAdd = new TransformOperation(); |
|||
|
|||
toAdd.Type = TransformOperation.OperationType.Translate; |
|||
toAdd.Data.Translate.X = x; |
|||
toAdd.Data.Translate.Y = y; |
|||
|
|||
toAdd.Bake(); |
|||
|
|||
_operations.Add(toAdd); |
|||
} |
|||
|
|||
public void AppendRotate(double angle) |
|||
{ |
|||
var toAdd = new TransformOperation(); |
|||
|
|||
toAdd.Type = TransformOperation.OperationType.Rotate; |
|||
toAdd.Data.Rotate.Angle = angle; |
|||
|
|||
toAdd.Bake(); |
|||
|
|||
_operations.Add(toAdd); |
|||
} |
|||
|
|||
public void AppendScale(double x, double y) |
|||
{ |
|||
var toAdd = new TransformOperation(); |
|||
|
|||
toAdd.Type = TransformOperation.OperationType.Scale; |
|||
toAdd.Data.Scale.X = x; |
|||
toAdd.Data.Scale.Y = y; |
|||
|
|||
toAdd.Bake(); |
|||
|
|||
_operations.Add(toAdd); |
|||
} |
|||
|
|||
public void AppendSkew(double x, double y) |
|||
{ |
|||
var toAdd = new TransformOperation(); |
|||
|
|||
toAdd.Type = TransformOperation.OperationType.Skew; |
|||
toAdd.Data.Skew.X = x; |
|||
toAdd.Data.Skew.Y = y; |
|||
|
|||
toAdd.Bake(); |
|||
|
|||
_operations.Add(toAdd); |
|||
} |
|||
|
|||
public void AppendMatrix(Matrix matrix) |
|||
{ |
|||
var toAdd = new TransformOperation(); |
|||
|
|||
toAdd.Type = TransformOperation.OperationType.Matrix; |
|||
toAdd.Matrix = matrix; |
|||
|
|||
_operations.Add(toAdd); |
|||
} |
|||
|
|||
public void AppendIdentity() |
|||
{ |
|||
var toAdd = new TransformOperation(); |
|||
|
|||
toAdd.Type = TransformOperation.OperationType.Identity; |
|||
|
|||
_operations.Add(toAdd); |
|||
} |
|||
|
|||
public void Append(TransformOperation toAdd) |
|||
{ |
|||
_operations.Add(toAdd); |
|||
} |
|||
|
|||
public TransformOperations Build() |
|||
{ |
|||
return new TransformOperations(_operations); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,463 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Media.Transformation |
|||
{ |
|||
public static class TransformParser |
|||
{ |
|||
private static readonly (string, TransformFunction)[] s_functionMapping = |
|||
{ |
|||
("translate", TransformFunction.Translate), |
|||
("translateX", TransformFunction.TranslateX), |
|||
("translateY", TransformFunction.TranslateY), |
|||
("scale", TransformFunction.Scale), |
|||
("scaleX", TransformFunction.ScaleX), |
|||
("scaleY", TransformFunction.ScaleY), |
|||
("skew", TransformFunction.Skew), |
|||
("skewX", TransformFunction.SkewX), |
|||
("skewY", TransformFunction.SkewY), |
|||
("rotate", TransformFunction.Rotate), |
|||
("matrix", TransformFunction.Matrix) |
|||
}; |
|||
|
|||
private static readonly (string, Unit)[] s_unitMapping = |
|||
{ |
|||
("deg", Unit.Degree), |
|||
("grad", Unit.Gradian), |
|||
("rad", Unit.Radian), |
|||
("turn", Unit.Turn), |
|||
("px", Unit.Pixel) |
|||
}; |
|||
|
|||
public static TransformOperations Parse(string s) |
|||
{ |
|||
void ThrowInvalidFormat() |
|||
{ |
|||
throw new FormatException($"Invalid transform string: '{s}'."); |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(s)) |
|||
{ |
|||
throw new ArgumentException(nameof(s)); |
|||
} |
|||
|
|||
var span = s.AsSpan().Trim(); |
|||
|
|||
if (span.Equals("none".AsSpan(), StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
return TransformOperations.Identity; |
|||
} |
|||
|
|||
var builder = TransformOperations.CreateBuilder(0); |
|||
|
|||
while (true) |
|||
{ |
|||
var beginIndex = span.IndexOf('('); |
|||
var endIndex = span.IndexOf(')'); |
|||
|
|||
if (beginIndex == -1 || endIndex == -1) |
|||
{ |
|||
ThrowInvalidFormat(); |
|||
} |
|||
|
|||
var namePart = span.Slice(0, beginIndex).Trim(); |
|||
|
|||
var function = ParseTransformFunction(in namePart); |
|||
|
|||
if (function == TransformFunction.Invalid) |
|||
{ |
|||
ThrowInvalidFormat(); |
|||
} |
|||
|
|||
var valuePart = span.Slice(beginIndex + 1, endIndex - beginIndex - 1).Trim(); |
|||
|
|||
ParseFunction(in valuePart, function, in builder); |
|||
|
|||
span = span.Slice(endIndex + 1); |
|||
|
|||
if (span.IsWhiteSpace()) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return builder.Build(); |
|||
} |
|||
|
|||
private static void ParseFunction( |
|||
in ReadOnlySpan<char> functionPart, |
|||
TransformFunction function, |
|||
in TransformOperations.Builder builder) |
|||
{ |
|||
static UnitValue ParseValue(ReadOnlySpan<char> part) |
|||
{ |
|||
int unitIndex = -1; |
|||
|
|||
for (int i = 0; i < part.Length; i++) |
|||
{ |
|||
char c = part[i]; |
|||
|
|||
if (char.IsDigit(c) || c == '-' || c == '.') |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
unitIndex = i; |
|||
break; |
|||
} |
|||
|
|||
Unit unit = Unit.None; |
|||
|
|||
if (unitIndex != -1) |
|||
{ |
|||
var unitPart = part.Slice(unitIndex, part.Length - unitIndex); |
|||
|
|||
unit = ParseUnit(unitPart); |
|||
|
|||
part = part.Slice(0, unitIndex); |
|||
} |
|||
|
|||
var value = double.Parse(part.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture); |
|||
|
|||
return new UnitValue(unit, value); |
|||
} |
|||
|
|||
static int ParseValuePair( |
|||
in ReadOnlySpan<char> part, |
|||
ref UnitValue leftValue, |
|||
ref UnitValue rightValue) |
|||
{ |
|||
var commaIndex = part.IndexOf(','); |
|||
|
|||
if (commaIndex != -1) |
|||
{ |
|||
var leftPart = part.Slice(0, commaIndex).Trim(); |
|||
var rightPart = part.Slice(commaIndex + 1, part.Length - commaIndex - 1).Trim(); |
|||
|
|||
leftValue = ParseValue(leftPart); |
|||
rightValue = ParseValue(rightPart); |
|||
|
|||
return 2; |
|||
} |
|||
|
|||
leftValue = ParseValue(part); |
|||
|
|||
return 1; |
|||
} |
|||
|
|||
static int ParseCommaDelimitedValues(ReadOnlySpan<char> part, in Span<UnitValue> outValues) |
|||
{ |
|||
int valueIndex = 0; |
|||
|
|||
while (true) |
|||
{ |
|||
if (valueIndex >= outValues.Length) |
|||
{ |
|||
throw new FormatException("Too many provided values."); |
|||
} |
|||
|
|||
var commaIndex = part.IndexOf(','); |
|||
|
|||
if (commaIndex == -1) |
|||
{ |
|||
if (!part.IsWhiteSpace()) |
|||
{ |
|||
outValues[valueIndex++] = ParseValue(part); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
var valuePart = part.Slice(0, commaIndex).Trim(); |
|||
|
|||
outValues[valueIndex++] = ParseValue(valuePart); |
|||
|
|||
part = part.Slice(commaIndex + 1, part.Length - commaIndex - 1); |
|||
} |
|||
|
|||
return valueIndex; |
|||
} |
|||
|
|||
switch (function) |
|||
{ |
|||
case TransformFunction.Scale: |
|||
case TransformFunction.ScaleX: |
|||
case TransformFunction.ScaleY: |
|||
{ |
|||
var scaleX = UnitValue.One; |
|||
var scaleY = UnitValue.One; |
|||
|
|||
int count = ParseValuePair(functionPart, ref scaleX, ref scaleY); |
|||
|
|||
if (count != 1 && (function == TransformFunction.ScaleX || function == TransformFunction.ScaleY)) |
|||
{ |
|||
ThrowFormatInvalidValueCount(function, 1); |
|||
} |
|||
|
|||
VerifyZeroOrUnit(function, in scaleX, Unit.None); |
|||
VerifyZeroOrUnit(function, in scaleY, Unit.None); |
|||
|
|||
if (function == TransformFunction.ScaleX) |
|||
{ |
|||
scaleY = UnitValue.Zero; |
|||
} |
|||
else if (function == TransformFunction.ScaleY) |
|||
{ |
|||
scaleY = scaleX; |
|||
scaleX = UnitValue.Zero; |
|||
} |
|||
else if (count == 1) |
|||
{ |
|||
scaleY = scaleX; |
|||
} |
|||
|
|||
builder.AppendScale(scaleX.Value, scaleY.Value); |
|||
|
|||
break; |
|||
} |
|||
case TransformFunction.Skew: |
|||
case TransformFunction.SkewX: |
|||
case TransformFunction.SkewY: |
|||
{ |
|||
var skewX = UnitValue.Zero; |
|||
var skewY = UnitValue.Zero; |
|||
|
|||
int count = ParseValuePair(functionPart, ref skewX, ref skewY); |
|||
|
|||
if (count != 1 && (function == TransformFunction.SkewX || function == TransformFunction.SkewY)) |
|||
{ |
|||
ThrowFormatInvalidValueCount(function, 1); |
|||
} |
|||
|
|||
VerifyZeroOrAngle(function, in skewX); |
|||
VerifyZeroOrAngle(function, in skewY); |
|||
|
|||
if (function == TransformFunction.SkewX) |
|||
{ |
|||
skewY = UnitValue.Zero; |
|||
} |
|||
else if (function == TransformFunction.SkewY) |
|||
{ |
|||
skewY = skewX; |
|||
skewX = UnitValue.Zero; |
|||
} |
|||
else if (count == 1) |
|||
{ |
|||
skewY = skewX; |
|||
} |
|||
|
|||
builder.AppendSkew(ToRadians(in skewX), ToRadians(in skewY)); |
|||
|
|||
break; |
|||
} |
|||
case TransformFunction.Rotate: |
|||
{ |
|||
var angle = UnitValue.Zero; |
|||
UnitValue _ = default; |
|||
|
|||
int count = ParseValuePair(functionPart, ref angle, ref _); |
|||
|
|||
if (count != 1) |
|||
{ |
|||
ThrowFormatInvalidValueCount(function, 1); |
|||
} |
|||
|
|||
VerifyZeroOrAngle(function, in angle); |
|||
|
|||
builder.AppendRotate(ToRadians(in angle)); |
|||
|
|||
break; |
|||
} |
|||
case TransformFunction.Translate: |
|||
case TransformFunction.TranslateX: |
|||
case TransformFunction.TranslateY: |
|||
{ |
|||
var translateX = UnitValue.Zero; |
|||
var translateY = UnitValue.Zero; |
|||
|
|||
int count = ParseValuePair(functionPart, ref translateX, ref translateY); |
|||
|
|||
if (count != 1 && (function == TransformFunction.TranslateX || function == TransformFunction.TranslateY)) |
|||
{ |
|||
ThrowFormatInvalidValueCount(function, 1); |
|||
} |
|||
|
|||
VerifyZeroOrUnit(function, in translateX, Unit.Pixel); |
|||
VerifyZeroOrUnit(function, in translateY, Unit.Pixel); |
|||
|
|||
if (function == TransformFunction.TranslateX) |
|||
{ |
|||
translateY = UnitValue.Zero; |
|||
} |
|||
else if (function == TransformFunction.TranslateY) |
|||
{ |
|||
translateY = translateX; |
|||
translateX = UnitValue.Zero; |
|||
} |
|||
else if (count == 1) |
|||
{ |
|||
translateY = translateX; |
|||
} |
|||
|
|||
builder.AppendTranslate(translateX.Value, translateY.Value); |
|||
|
|||
break; |
|||
} |
|||
case TransformFunction.Matrix: |
|||
{ |
|||
Span<UnitValue> values = stackalloc UnitValue[6]; |
|||
|
|||
int count = ParseCommaDelimitedValues(functionPart, in values); |
|||
|
|||
if (count != 6) |
|||
{ |
|||
ThrowFormatInvalidValueCount(function, 6); |
|||
} |
|||
|
|||
foreach (UnitValue value in values) |
|||
{ |
|||
VerifyZeroOrUnit(function, value, Unit.None); |
|||
} |
|||
|
|||
var matrix = new Matrix( |
|||
values[0].Value, |
|||
values[1].Value, |
|||
values[2].Value, |
|||
values[3].Value, |
|||
values[4].Value, |
|||
values[5].Value); |
|||
|
|||
builder.AppendMatrix(matrix); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void VerifyZeroOrUnit(TransformFunction function, in UnitValue value, Unit unit) |
|||
{ |
|||
bool isZero = value.Unit == Unit.None && value.Value == 0d; |
|||
|
|||
if (!isZero && value.Unit != unit) |
|||
{ |
|||
ThrowFormatInvalidValue(function, in value); |
|||
} |
|||
} |
|||
|
|||
private static void VerifyZeroOrAngle(TransformFunction function, in UnitValue value) |
|||
{ |
|||
if (value.Value != 0d && !IsAngleUnit(value.Unit)) |
|||
{ |
|||
ThrowFormatInvalidValue(function, in value); |
|||
} |
|||
} |
|||
|
|||
private static bool IsAngleUnit(Unit unit) |
|||
{ |
|||
switch (unit) |
|||
{ |
|||
case Unit.Radian: |
|||
case Unit.Degree: |
|||
case Unit.Turn: |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private static void ThrowFormatInvalidValue(TransformFunction function, in UnitValue value) |
|||
{ |
|||
var unitString = value.Unit == Unit.None ? string.Empty : value.Unit.ToString(); |
|||
|
|||
throw new FormatException($"Invalid value {value.Value} {unitString} for {function}"); |
|||
} |
|||
|
|||
private static void ThrowFormatInvalidValueCount(TransformFunction function, int count) |
|||
{ |
|||
throw new FormatException($"Invalid format. {function} expects {count} value(s)."); |
|||
} |
|||
|
|||
private static Unit ParseUnit(in ReadOnlySpan<char> part) |
|||
{ |
|||
foreach (var (name, unit) in s_unitMapping) |
|||
{ |
|||
if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
return unit; |
|||
} |
|||
} |
|||
|
|||
throw new FormatException($"Invalid unit: {part.ToString()}"); |
|||
} |
|||
|
|||
private static TransformFunction ParseTransformFunction(in ReadOnlySpan<char> part) |
|||
{ |
|||
foreach (var (name, transformFunction) in s_functionMapping) |
|||
{ |
|||
if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
return transformFunction; |
|||
} |
|||
} |
|||
|
|||
return TransformFunction.Invalid; |
|||
} |
|||
|
|||
private static double ToRadians(in UnitValue value) |
|||
{ |
|||
return value.Unit switch |
|||
{ |
|||
Unit.Radian => value.Value, |
|||
Unit.Gradian => MathUtilities.Grad2Rad(value.Value), |
|||
Unit.Degree => MathUtilities.Deg2Rad(value.Value), |
|||
Unit.Turn => MathUtilities.Turn2Rad(value.Value), |
|||
_ => value.Value |
|||
}; |
|||
} |
|||
|
|||
private enum Unit |
|||
{ |
|||
None, |
|||
Pixel, |
|||
Radian, |
|||
Gradian, |
|||
Degree, |
|||
Turn |
|||
} |
|||
|
|||
private readonly struct UnitValue |
|||
{ |
|||
public readonly Unit Unit; |
|||
public readonly double Value; |
|||
|
|||
public UnitValue(Unit unit, double value) |
|||
{ |
|||
Unit = unit; |
|||
Value = value; |
|||
} |
|||
|
|||
public static UnitValue Zero => new UnitValue(Unit.None, 0); |
|||
|
|||
public static UnitValue One => new UnitValue(Unit.None, 1); |
|||
} |
|||
|
|||
private enum TransformFunction |
|||
{ |
|||
Invalid, |
|||
Translate, |
|||
TranslateX, |
|||
TranslateY, |
|||
Scale, |
|||
ScaleX, |
|||
ScaleY, |
|||
Skew, |
|||
SkewX, |
|||
SkewY, |
|||
Rotate, |
|||
Matrix |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,128 @@ |
|||
using Avalonia.Media.Transformation; |
|||
using Avalonia.Utilities; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Visuals.UnitTests.Media |
|||
{ |
|||
public class TransformOperationsTests |
|||
{ |
|||
[Fact] |
|||
public void Can_Parse_Compound_Operations() |
|||
{ |
|||
var data = "scale(1,2) translate(3px,4px) rotate(5deg) skew(6deg,7deg)"; |
|||
|
|||
var transform = TransformOperations.Parse(data); |
|||
|
|||
var operations = transform.Operations; |
|||
|
|||
Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type); |
|||
Assert.Equal(1, operations[0].Data.Scale.X); |
|||
Assert.Equal(2, operations[0].Data.Scale.Y); |
|||
|
|||
Assert.Equal(TransformOperation.OperationType.Translate, operations[1].Type); |
|||
Assert.Equal(3, operations[1].Data.Translate.X); |
|||
Assert.Equal(4, operations[1].Data.Translate.Y); |
|||
|
|||
Assert.Equal(TransformOperation.OperationType.Rotate, operations[2].Type); |
|||
Assert.Equal(MathUtilities.Deg2Rad(5), operations[2].Data.Rotate.Angle); |
|||
|
|||
Assert.Equal(TransformOperation.OperationType.Skew, operations[3].Type); |
|||
Assert.Equal(MathUtilities.Deg2Rad(6), operations[3].Data.Skew.X); |
|||
Assert.Equal(MathUtilities.Deg2Rad(7), operations[3].Data.Skew.Y); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Can_Parse_Matrix_Operation() |
|||
{ |
|||
var data = "matrix(1,2,3,4,5,6)"; |
|||
|
|||
var transform = TransformOperations.Parse(data); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0d, 10d, 0d)] |
|||
[InlineData(0.5d, 5d, 10d)] |
|||
[InlineData(1d, 0d, 20d)] |
|||
public void Can_Interpolate_Translation(double progress, double x, double y) |
|||
{ |
|||
var from = TransformOperations.Parse("translateX(10px)"); |
|||
var to = TransformOperations.Parse("translateY(20px)"); |
|||
|
|||
var interpolated = TransformOperations.Interpolate(from, to, progress); |
|||
|
|||
var operations = interpolated.Operations; |
|||
|
|||
Assert.Single(operations); |
|||
Assert.Equal(TransformOperation.OperationType.Translate, operations[0].Type); |
|||
Assert.Equal(x, operations[0].Data.Translate.X); |
|||
Assert.Equal(y, operations[0].Data.Translate.Y); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0d, 10d, 0d)] |
|||
[InlineData(0.5d, 5d, 10d)] |
|||
[InlineData(1d, 0d, 20d)] |
|||
public void Can_Interpolate_Scale(double progress, double x, double y) |
|||
{ |
|||
var from = TransformOperations.Parse("scaleX(10)"); |
|||
var to = TransformOperations.Parse("scaleY(20)"); |
|||
|
|||
var interpolated = TransformOperations.Interpolate(from, to, progress); |
|||
|
|||
var operations = interpolated.Operations; |
|||
|
|||
Assert.Single(operations); |
|||
Assert.Equal(TransformOperation.OperationType.Scale, operations[0].Type); |
|||
Assert.Equal(x, operations[0].Data.Scale.X); |
|||
Assert.Equal(y, operations[0].Data.Scale.Y); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0d, 10d, 0d)] |
|||
[InlineData(0.5d, 5d, 10d)] |
|||
[InlineData(1d, 0d, 20d)] |
|||
public void Can_Interpolate_Skew(double progress, double x, double y) |
|||
{ |
|||
var from = TransformOperations.Parse("skewX(10deg)"); |
|||
var to = TransformOperations.Parse("skewY(20deg)"); |
|||
|
|||
var interpolated = TransformOperations.Interpolate(from, to, progress); |
|||
|
|||
var operations = interpolated.Operations; |
|||
|
|||
Assert.Single(operations); |
|||
Assert.Equal(TransformOperation.OperationType.Skew, operations[0].Type); |
|||
Assert.Equal(MathUtilities.Deg2Rad(x), operations[0].Data.Skew.X); |
|||
Assert.Equal(MathUtilities.Deg2Rad(y), operations[0].Data.Skew.Y); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0d, 10d)] |
|||
[InlineData(0.5d, 15d)] |
|||
[InlineData(1d,20d)] |
|||
public void Can_Interpolate_Rotation(double progress, double angle) |
|||
{ |
|||
var from = TransformOperations.Parse("rotate(10deg)"); |
|||
var to = TransformOperations.Parse("rotate(20deg)"); |
|||
|
|||
var interpolated = TransformOperations.Interpolate(from, to, progress); |
|||
|
|||
var operations = interpolated.Operations; |
|||
|
|||
Assert.Single(operations); |
|||
Assert.Equal(TransformOperation.OperationType.Rotate, operations[0].Type); |
|||
Assert.Equal(MathUtilities.Deg2Rad(angle), operations[0].Data.Rotate.Angle); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Can_Interpolate_Matrix() |
|||
{ |
|||
double progress = 0.5d; |
|||
|
|||
var from = TransformOperations.Parse("rotate(45deg)"); |
|||
var to = TransformOperations.Parse("translate(100px, 100px) rotate(1215deg)"); |
|||
|
|||
var interpolated = TransformOperations.Interpolate(from, to, progress); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue