using System; using System.Globalization; using System.Linq; using System.Numerics; using Avalonia.Utilities; namespace Avalonia { /// /// A 3x3 matrix. /// /// Matrix layout: /// | 1st col | 2nd col | 3r col | /// 1st row | scaleX | skewY | perspX | /// 2nd row | skewX | scaleY | perspY | /// 3rd row | transX | transY | perspZ | /// /// Note: Skia.SkMatrix uses a transposed layout (where for example skewX/skewY and persp0/transX are swapped). /// #if !BUILDTASK public #endif readonly struct Matrix : IEquatable { private readonly double _m11; private readonly double _m12; private readonly double _m13; private readonly double _m21; private readonly double _m22; private readonly double _m23; private readonly double _m31; private readonly double _m32; private readonly double _m33; /// /// Initializes a new instance of the struct (equivalent to a 2x3 Matrix without perspective). /// /// The first element of the first row. /// The second element of the first row. /// The first element of the second row. /// The second element of the second row. /// The first element of the third row. /// The second element of the third row. public Matrix( double scaleX, double skewY, double skewX, double scaleY, double offsetX, double offsetY) : this( scaleX, skewY, 0, skewX, scaleY, 0, offsetX, offsetY, 1) { } /// /// Initializes a new instance of the struct. /// /// The first element of the first row. /// The second element of the first row. /// The third element of the first row. /// The first element of the second row. /// The second element of the second row. /// The third element of the second row. /// The first element of the third row. /// The second element of the third row. /// The third element of the third row. public Matrix( double scaleX, double skewY, double perspX, double skewX, double scaleY, double perspY, double offsetX, double offsetY, double perspZ) { _m11 = scaleX; _m12 = skewY; _m13 = perspX; _m21 = skewX; _m22 = scaleY; _m23 = perspY; _m31 = offsetX; _m32 = offsetY; _m33 = perspZ; } /// /// Returns the multiplicative identity matrix. /// public static Matrix Identity { get; } = new Matrix( 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); /// /// Returns whether the matrix is the identity matrix. /// public bool IsIdentity => Equals(Identity); /// /// HasInverse Property - returns true if this matrix is invertible, false otherwise. /// public bool HasInverse => !MathUtilities.IsZero(GetDeterminant()); /// /// The first element of the first row (scaleX). /// public double M11 => _m11; /// /// The second element of the first row (skewY). /// public double M12 => _m12; /// /// The third element of the first row (perspX: input x-axis perspective factor). /// public double M13 => _m13; /// /// The first element of the second row (skewX). /// public double M21 => _m21; /// /// The second element of the second row (scaleY). /// public double M22 => _m22; /// /// The third element of the second row (perspY: input y-axis perspective factor). /// public double M23 => _m23; /// /// The first element of the third row (offsetX/translateX). /// public double M31 => _m31; /// /// The second element of the third row (offsetY/translateY). /// public double M32 => _m32; /// /// The third element of the third row (perspZ: perspective scale factor). /// public double M33 => _m33; /// /// Multiplies two matrices together and returns the resulting matrix. /// /// The first source matrix. /// The second source matrix. /// The product matrix. public static Matrix operator *(Matrix value1, Matrix value2) { return new Matrix( (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)); } /// /// Negates the given matrix by multiplying all values by -1. /// /// The source matrix. /// The negated matrix. public static Matrix operator -(Matrix value) { return value.Invert(); } /// /// Returns a boolean indicating whether the given matrices are equal. /// /// The first source matrix. /// The second source matrix. /// True if the matrices are equal; False otherwise. public static bool operator ==(Matrix value1, Matrix value2) { return value1.Equals(value2); } /// /// Returns a boolean indicating whether the given matrices are not equal. /// /// The first source matrix. /// The second source matrix. /// True if the matrices are not equal; False if they are equal. public static bool operator !=(Matrix value1, Matrix value2) { return !value1.Equals(value2); } /// /// Creates a rotation matrix using the given rotation in radians. /// /// The amount of rotation, in radians. /// A rotation matrix. public static Matrix CreateRotation(double radians) { double cos = Math.Cos(radians); double sin = Math.Sin(radians); return new Matrix(cos, sin, -sin, cos, 0, 0); } /// /// Creates a skew matrix from the given axis skew angles in radians. /// /// The amount of skew along the X-axis, in radians. /// The amount of skew along the Y-axis, in radians. /// A rotation matrix. public static Matrix CreateSkew(double xAngle, double yAngle) { double tanX = Math.Tan(xAngle); double tanY = Math.Tan(yAngle); return new Matrix(1.0, tanY, tanX, 1.0, 0.0, 0.0); } /// /// Creates a scale matrix from the given X and Y components. /// /// Value to scale by on the X-axis. /// Value to scale by on the Y-axis. /// A scaling matrix. public static Matrix CreateScale(double xScale, double yScale) { return new Matrix(xScale, 0, 0, yScale, 0, 0); } /// /// Creates a scale matrix from the given vector scale. /// /// The scale to use. /// A scaling matrix. public static Matrix CreateScale(Vector scales) { return CreateScale(scales.X, scales.Y); } /// /// Creates a translation matrix from the given vector. /// /// The translation position. /// A translation matrix. public static Matrix CreateTranslation(Vector position) { return CreateTranslation(position.X, position.Y); } /// /// Creates a translation matrix from the given X and Y components. /// /// The X position. /// The Y position. /// A translation matrix. public static Matrix CreateTranslation(double xPosition, double yPosition) { return new Matrix(1.0, 0.0, 0.0, 1.0, xPosition, yPosition); } /// /// Converts an angle in degrees to radians. /// /// The angle in degrees. /// The angle in radians. public static double ToRadians(double angle) { return angle * 0.0174532925; } /// /// Appends another matrix as post-multiplication operation. /// Equivalent to this * value; /// /// A matrix. /// Post-multiplied matrix. public Matrix Append(Matrix value) { return this * value; } /// /// Prepends another matrix as pre-multiplication operation. /// Equivalent to value * this; /// /// A matrix. /// Pre-multiplied matrix. public Matrix Prepend(Matrix value) { return value * this; } /// /// Calculates the determinant for this matrix. /// /// The determinant. /// /// The determinant is calculated by expanding the matrix with a third column whose /// values are (0,0,1). /// public double GetDeterminant() { // implemented using "Laplace expansion": return _m11 * (_m22 * _m33 - _m23 * _m32) - _m12 * (_m21 * _m33 - _m23 * _m31) + _m13 * (_m21 * _m32 - _m22 * _m31); } /// /// Transforms the point with the matrix /// /// The point to be transformed /// The transformed point public Point Transform(Point p) { Point transformedResult; // If this matrix contains a non-affine transform with need to extend // the point to a 3D vector and flatten it back for 2d display // by multiplying X and Y with the inverse of the Z axis. // The code below also works with affine transformations, but for performance (and compatibility) // reasons we will use the more complex calculation only if necessary if (ContainsPerspective()) { var m44 = new Matrix4x4( (float)M11, (float)M12, (float)M13, 0, (float)M21, (float)M22, (float)M23, 0, (float)M31, (float)M32, (float)M33, 0, 0, 0, 0, 1 ); var vector = new Vector3((float)p.X, (float)p.Y, 1); var transformedVector = Vector3.Transform(vector, m44); var z = 1 / transformedVector.Z; transformedResult = new Point(transformedVector.X * z, transformedVector.Y * z); } else { return new Point( (p.X * M11) + (p.Y * M21) + M31, (p.X * M12) + (p.Y * M22) + M32); } return transformedResult; } /// /// Returns a boolean indicating whether the matrix is equal to the other given matrix. /// /// The other matrix to test equality against. /// True if this matrix is equal to other; False otherwise. public bool Equals(Matrix other) { // 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 && _m33 == other.M33; // ReSharper restore CompareOfFloatsByEqualityOperator } /// /// Returns a boolean indicating whether the given Object is equal to this matrix instance. /// /// The Object to compare against. /// True if the Object is equal to this matrix; False otherwise. public override bool Equals(object? obj) => obj is Matrix other && Equals(other); /// /// Returns the hash code for this instance. /// /// The hash code. public override int GetHashCode() { return (_m11, _m12, _m13, _m21, _m22, _m23, _m31, _m32, _m33).GetHashCode(); } /// /// Determines if the current matrix contains perspective (non-affine) transforms (true) or only (affine) transforms that could be mapped into an 2x3 matrix (false). /// public bool ContainsPerspective() { // ReSharper disable CompareOfFloatsByEqualityOperator return _m13 != 0 || _m23 != 0 || _m33 != 1; // ReSharper restore CompareOfFloatsByEqualityOperator } /// /// Returns a String representing this matrix instance. /// /// The string representation. 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, msg, values.Select((v) => v.ToString(ci)).ToArray()); } /// /// Attempts to invert the Matrix. /// /// The inverted matrix or when matrix is not invertible. public bool TryInvert(out Matrix inverted) { double d = GetDeterminant(); if (MathUtilities.IsZero(d)) { inverted = default; return false; } var invdet = 1 / d; inverted = new Matrix( (_m22 * _m33 - _m32 * _m23) * invdet, (_m13 * _m32 - _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, (_m31 * _m12 - _m11 * _m32) * invdet, (_m11 * _m22 - _m21 * _m12) * invdet ); return true; } /// /// Inverts the Matrix. /// /// Matrix is not invertible. /// The inverted matrix. public Matrix Invert() { if (!TryInvert(out var inverted)) { throw new InvalidOperationException("Transform is not invertible."); } return inverted; } /// /// Parses a string. /// /// Six or nine comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY[, perspX, perspY, perspZ]) that describe the new /// The . public static Matrix Parse(string s) { // initialize to satisfy compiler - only used when retrieved from string. double v8 = 0; double v9 = 0; using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Matrix.")) { 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 persp = tokenizer.TryReadDouble(out var v7); persp = persp && tokenizer.TryReadDouble(out v8); persp = persp && tokenizer.TryReadDouble(out v9); if (persp) return new Matrix(v1, v2, v7, v3, v4, v8, v5, v6, v9); else return new Matrix(v1, v2, v3, v4, v5, v6); } } /// /// Decomposes given matrix into transform operations. /// /// Matrix to decompose. /// Decomposed matrix. /// The status of the operation. public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed) { decomposed = default; var determinant = matrix.GetDeterminant(); if (MathUtilities.IsZero(determinant) || matrix.ContainsPerspective()) { return false; } var m11 = matrix.M11; var m21 = matrix.M21; var m12 = matrix.M12; var m22 = matrix.M22; // Translation. decomposed.Translate = new Vector(matrix.M31, matrix.M32); // Scale sign. var scaleX = 1d; var scaleY = 1d; if (determinant < 0) { if (m11 < m22) { scaleX *= -1d; } else { scaleY *= -1d; } } // X Scale. scaleX *= Math.Sqrt(m11 * m11 + m12 * m12); m11 /= scaleX; m12 /= scaleX; // XY Shear. double scaledShear = m11 * m21 + m12 * m22; m21 -= m11 * scaledShear; m22 -= m12 * scaledShear; // Y Scale. scaleY *= Math.Sqrt(m21 * m21 + m22 * m22); decomposed.Scale = new Vector(scaleX, scaleY); decomposed.Skew = new Vector(scaledShear / scaleY, 0d); decomposed.Angle = Math.Atan2(m12, m11); return true; } public struct Decomposed { public Vector Translate; public Vector Scale; public Vector Skew; public double Angle; } } }