locked
Convert a MatrixTransform into TransformGroup RRS feed

  • Question

  • I am converting a custom vector graphics format into SilverLight and I am using Blend 2.5 to compare the results. The custom format uses a matrix transform which corresponds with the MatrixTransform in SilverLight. When I compare the results in Blend, I noticed that Blend can disect the coefficients and break the MatrixTransform into a TransformGroup with ScaleTransform, SkewTransform, RotateTransform, and TranslateTransform. For example, the output of my conversion contains the following transform:

    <Line.RenderTransform>
      <TransformGroup>
        <MatrixTransform>
          <MatrixTransform.Matrix>
            <Matrix M11="0.8660253" M12="0.4999999"
                       M21="-0.5000002" M22="0.8660254"
                       OffsetX="83.49355" OffsetY="-111.6026" />
          </MatrixTransform.Matrix>
        </MatrixTransform>
      </TransformGroup>
    </Line.RenderTransform>
    

    If I modify the transform in Blend, I end up with the following (equivalent) transform (although some of the coefficients should be compared to tolerance values and rounded accordingly):

    <Line.RenderTransform>
      <TransformGroup>
        <ScaleTransform ScaleX="0.99999988079071045" ScaleY="1.0000001192092896"/>
        <SkewTransform AngleX="-1.1601466212596279E-05"/>
        <RotateTransform Angle="30"/>
        <TranslateTransform X="83.4935531616211" Y="-111.60260009765625"/>
      </TransformGroup>
    </Line.RenderTransform>
    

    I started a manual conversion, but I was wondering if anyone has a conversion routine or knows where to find one. Thanks.

    Thursday, July 10, 2008 1:38 PM

Answers

  • Yes. You're right. After looking at Blend's code in Reflector, I agree this can be done. But it's quite complex, so I don't understand the algorithm very well. Maybe there're some mistakes in my description...

    First let's forget the TranslateTransform, since it's the simplest. You can get it from OffsetX/Y property of the matrix. Let's say our matrix is (the actual numbers are not important):

    |0.68    0.42    0|

    |0.35    0.84    0|

    |0        0          1|

    The first thing to do is to get the new coordinate system. We have two vectors in the original world: X axis: 1,0; Y axis: 0,1. After the transform (multiply the vectors with the matrix), the new axises are: X axis: 0.68, 0.42; Y axis: 0.35, 0.84. Now let's make some assumptions, or we'll never get the result. The length (or mod if you like to call it this way) of the new x axis is 0.8, and the length of the new y axis is 0.91. Let's say a temporary ScaleTransform is ScaleX = 0.8, ScaleY = 0.91. Let's assume at least one of ScaleX or ScaleY in this temporary transform is the same as the original ScaleTransform.

    Now let's find the angle between the new axises and the old ones (with some vector math). The result is: AngleX(NewX, OldX) = 31.7; AngleY(NewY, OldY) = -22.6 (clockwise is positive and counter-clockwise is negative). As you can see, in this case, the absolute value of AngleX is larger than AngleY, so our RotateTransform's Angle should come from y (-22.6) (Again we have to make some assumptions). This also leads us to regard the ScaleY in our former assumption is the same as ScaleY in the original ScaleTransform. But ScaleX may be different. To calculate the ScaleX, we should do the following:

    Find the unit vector of NewY. Then find the length of its projection on NewX (with a dot product). Then multiply the unit vector of NewY with the length of its projection on NewX (so you have a vector, let's call it yProjection, whose direction is the same as the new y axis, and length represents the proportion of y/x), and use NewX to minus the result. The final result is a vector from the endpoint of the new x axis, to the endpoint of the yProjection. This vector's length is our ScaleX. Note I don't understand this part very well, so there may be some mistakes in this explanation...

    Now that we have Scale and Rotate, it's quite easy to compute Skew.

    There're quite a few assumptions in this algorithm, and I don't understand them quite well... But at least the result is correct.

    Here's the code from Reflector for your reference. Something to note: The tolerance factor is 2.2204460492503131E-16. But this doesn't work in our sample. I think you can just set tolerance to 0. The various vectors are:

    vector: The new x axis (at least after multiplied with the matrix).

    vector2: The new y axis.

    vector3: Used together with the tolerance factor. Normally you can ignore this part.

    vector4: The unit vector of the new x axis. If the absolute value of AngleX is smaller than AngleY, you go to this branch.

    vector5: The unit vector of the new y axis. If the absolute value of AngleX is larger than AngleY, you go to this branch.

    vector7: A vector from the endpoint of the new y axis to the endpoint of vector4. Its length is the actual ScaleY.

    vector9: A vector from the endpoint of the new x axis to the endpoint of vector5. Its length is the actual ScaleX.

    And finally, there's no Vector structure in Silverlight. But you can copy it from the desktop version with Reflector.

    private void ConvertGenericTransform(Matrix transform)
    {
        this.Center = new Point(0.0, 0.0);
        this.Skew = new Vector(0.0, 0.0);
        this.RotationAngle = 0.0;
        Point point = new Point(0.0, 0.0);
        Vector vector = new Vector(1.0, 0.0);
        Vector vector2 = new Vector(0.0, 1.0);
        vector *= transform;
        vector2 *= transform;
        point *= transform;
        this.Translation = new Vector(point.X, point.Y);
        this.Scale = new Vector(vector.Length, vector2.Length);
        bool flag = vector.LengthSquared > tolerance;
        bool flag2 = vector2.LengthSquared > tolerance;
        double angle = 0.0;
        double num2 = 0.0;
        if (flag)
        {
            angle = this.GetAngle(new Vector(1.0, 0.0), vector);
        }
        if (flag2)
        {
            num2 = this.GetAngle(new Vector(0.0, 1.0), vector2);
        }
        if ((flag && flag2) && (Math.Abs(Vector.CrossProduct(vector, vector2)) <= tolerance))
        {
            Vector vector3 = (Math.Abs(angle) <= Math.Abs(num2)) ? vector : vector2;
            this.Skew = new Vector(45.0, 45.0);
            this.ScaleX *= 0.70710678118654757;
            this.ScaleY *= 0.70710678118654757;
            this.RotationAngle = this.GetAngle(new Vector(1.0, 1.0), vector3);
            if ((vector * vector3) < 0.0)
            {
                this.ScaleX = -this.ScaleX;
            }
            if ((vector2 * vector3) < 0.0)
            {
                this.ScaleY = -this.ScaleY;
            }
        }
        else if (flag || flag2)
        {
            if (!flag2 || (flag && (Math.Abs(angle) <= Math.Abs(num2))))
            {
                Vector vector4 = (Vector) (vector / this.Scale.X);
                Vector vector7 = vector2 - ((Vector) (vector4 * ((vector4.X * vector2.X) + (vector4.Y * vector2.Y))));
                this.ScaleY = vector7.Length;
                this.RotationAngle = angle;
                if (flag2)
                {
                    double num3 = this.GetAngle(vector, vector2);
                    if (num3 < 0.0)
                    {
                        this.ScaleY = -this.ScaleY;
                        this.Skew = new Vector(-num3 - 90.0, 0.0);
                    }
                    else
                    {
                        this.Skew = new Vector(90.0 - num3, 0.0);
                    }
                }
            }
            else
            {
                Vector vector5 = (Vector) (vector2 / this.Scale.Y);
                Vector vector9 = vector - ((Vector) (vector5 * ((vector5.X * vector.X) + (vector5.Y * vector.Y))));
                this.ScaleX = vector9.Length;
                this.RotationAngle = num2;
                if (flag)
                {
                    double num4 = this.GetAngle(vector2, vector);
                    if (num4 > 0.0)
                    {
                        this.ScaleX = -this.ScaleX;
                        this.Skew = new Vector(0.0, num4 - 90.0);
                    }
                    else
                    {
                        this.Skew = new Vector(0.0, num4 + 90.0);
                    }
                }
            }
        }
    }
    
    
    

    private double GetAngle(Vector v1, Vector v2)
    {
        return ((Math.Atan2((v1.X * v2.Y) - (v1.Y * v2.X), (v1.X * v2.X) + (v1.Y * v2.Y)) * 180.0) / 3.1415926535897931);
    }

    Wednesday, July 23, 2008 1:17 AM

All replies

  •  

    Unfortunately there's no way to accomplish this if you have all those four transforms. RotateTransform will affect M11, M12, M21, M22. The simplest form is:

    |cos a    sin a        0|

    |-sin a    cos a       0|

    |0          0             1|

    ScaleTransform will affect M11 and M22. The simplest form is:

    |Sx    0       0|

    |0     Sy      0|

    |0      0       1|

    Even if you only have those two transforms, the result will be Matrix1*Matrix2=

    |(cos a)*Sx     (Sin a)*Sy      0|

    |-(sin a)*Sx    (cos a)*Sy      0|

    |0                  0                     1|

    Let's define a as (cos a), b as (sin a), c as Sx, d as Sy. So you have:

    ac = M11

    bd = M12

    -bc = M21

    ad = M22

    This equation group has no solution. That means you can't figure out each individual part of the transform group.

    Monday, July 14, 2008 5:16 AM
  • This equation group has no solution. That means you can't figure out each individual part of the transform group.

    Yes, I agree the equation group does not have a solution. However I believe it is possible by making assumptions depending on the shape being transformed, or if the transform group contains only one or two transformations. In the example I listed, Expression Blend 2.5 preview automatically changed the MatrixTransform into individual transforms when I adjusted the rotation angle (the scale is essential 1.0 and the skew effectively 0.0). I am interested in how Blend is able to do this.

    I believe the source for my files for the MatrixTransforms are only using rotation, but perhaps a non-standard form. For example, a line that was rotated 245 degrees had coefficients of 0.4226, 0.9063, 0.9063, -0.4226 instead of -0.4226, -0.9063, 0.9063, -0.4226. With lines, it is simple enough to check the slope and distance after the transform and work backwards. With more complex shapes it may not be possible.

    Monday, July 21, 2008 2:04 PM
  • Yes. You're right. After looking at Blend's code in Reflector, I agree this can be done. But it's quite complex, so I don't understand the algorithm very well. Maybe there're some mistakes in my description...

    First let's forget the TranslateTransform, since it's the simplest. You can get it from OffsetX/Y property of the matrix. Let's say our matrix is (the actual numbers are not important):

    |0.68    0.42    0|

    |0.35    0.84    0|

    |0        0          1|

    The first thing to do is to get the new coordinate system. We have two vectors in the original world: X axis: 1,0; Y axis: 0,1. After the transform (multiply the vectors with the matrix), the new axises are: X axis: 0.68, 0.42; Y axis: 0.35, 0.84. Now let's make some assumptions, or we'll never get the result. The length (or mod if you like to call it this way) of the new x axis is 0.8, and the length of the new y axis is 0.91. Let's say a temporary ScaleTransform is ScaleX = 0.8, ScaleY = 0.91. Let's assume at least one of ScaleX or ScaleY in this temporary transform is the same as the original ScaleTransform.

    Now let's find the angle between the new axises and the old ones (with some vector math). The result is: AngleX(NewX, OldX) = 31.7; AngleY(NewY, OldY) = -22.6 (clockwise is positive and counter-clockwise is negative). As you can see, in this case, the absolute value of AngleX is larger than AngleY, so our RotateTransform's Angle should come from y (-22.6) (Again we have to make some assumptions). This also leads us to regard the ScaleY in our former assumption is the same as ScaleY in the original ScaleTransform. But ScaleX may be different. To calculate the ScaleX, we should do the following:

    Find the unit vector of NewY. Then find the length of its projection on NewX (with a dot product). Then multiply the unit vector of NewY with the length of its projection on NewX (so you have a vector, let's call it yProjection, whose direction is the same as the new y axis, and length represents the proportion of y/x), and use NewX to minus the result. The final result is a vector from the endpoint of the new x axis, to the endpoint of the yProjection. This vector's length is our ScaleX. Note I don't understand this part very well, so there may be some mistakes in this explanation...

    Now that we have Scale and Rotate, it's quite easy to compute Skew.

    There're quite a few assumptions in this algorithm, and I don't understand them quite well... But at least the result is correct.

    Here's the code from Reflector for your reference. Something to note: The tolerance factor is 2.2204460492503131E-16. But this doesn't work in our sample. I think you can just set tolerance to 0. The various vectors are:

    vector: The new x axis (at least after multiplied with the matrix).

    vector2: The new y axis.

    vector3: Used together with the tolerance factor. Normally you can ignore this part.

    vector4: The unit vector of the new x axis. If the absolute value of AngleX is smaller than AngleY, you go to this branch.

    vector5: The unit vector of the new y axis. If the absolute value of AngleX is larger than AngleY, you go to this branch.

    vector7: A vector from the endpoint of the new y axis to the endpoint of vector4. Its length is the actual ScaleY.

    vector9: A vector from the endpoint of the new x axis to the endpoint of vector5. Its length is the actual ScaleX.

    And finally, there's no Vector structure in Silverlight. But you can copy it from the desktop version with Reflector.

    private void ConvertGenericTransform(Matrix transform)
    {
        this.Center = new Point(0.0, 0.0);
        this.Skew = new Vector(0.0, 0.0);
        this.RotationAngle = 0.0;
        Point point = new Point(0.0, 0.0);
        Vector vector = new Vector(1.0, 0.0);
        Vector vector2 = new Vector(0.0, 1.0);
        vector *= transform;
        vector2 *= transform;
        point *= transform;
        this.Translation = new Vector(point.X, point.Y);
        this.Scale = new Vector(vector.Length, vector2.Length);
        bool flag = vector.LengthSquared > tolerance;
        bool flag2 = vector2.LengthSquared > tolerance;
        double angle = 0.0;
        double num2 = 0.0;
        if (flag)
        {
            angle = this.GetAngle(new Vector(1.0, 0.0), vector);
        }
        if (flag2)
        {
            num2 = this.GetAngle(new Vector(0.0, 1.0), vector2);
        }
        if ((flag && flag2) && (Math.Abs(Vector.CrossProduct(vector, vector2)) <= tolerance))
        {
            Vector vector3 = (Math.Abs(angle) <= Math.Abs(num2)) ? vector : vector2;
            this.Skew = new Vector(45.0, 45.0);
            this.ScaleX *= 0.70710678118654757;
            this.ScaleY *= 0.70710678118654757;
            this.RotationAngle = this.GetAngle(new Vector(1.0, 1.0), vector3);
            if ((vector * vector3) < 0.0)
            {
                this.ScaleX = -this.ScaleX;
            }
            if ((vector2 * vector3) < 0.0)
            {
                this.ScaleY = -this.ScaleY;
            }
        }
        else if (flag || flag2)
        {
            if (!flag2 || (flag && (Math.Abs(angle) <= Math.Abs(num2))))
            {
                Vector vector4 = (Vector) (vector / this.Scale.X);
                Vector vector7 = vector2 - ((Vector) (vector4 * ((vector4.X * vector2.X) + (vector4.Y * vector2.Y))));
                this.ScaleY = vector7.Length;
                this.RotationAngle = angle;
                if (flag2)
                {
                    double num3 = this.GetAngle(vector, vector2);
                    if (num3 < 0.0)
                    {
                        this.ScaleY = -this.ScaleY;
                        this.Skew = new Vector(-num3 - 90.0, 0.0);
                    }
                    else
                    {
                        this.Skew = new Vector(90.0 - num3, 0.0);
                    }
                }
            }
            else
            {
                Vector vector5 = (Vector) (vector2 / this.Scale.Y);
                Vector vector9 = vector - ((Vector) (vector5 * ((vector5.X * vector.X) + (vector5.Y * vector.Y))));
                this.ScaleX = vector9.Length;
                this.RotationAngle = num2;
                if (flag)
                {
                    double num4 = this.GetAngle(vector2, vector);
                    if (num4 > 0.0)
                    {
                        this.ScaleX = -this.ScaleX;
                        this.Skew = new Vector(0.0, num4 - 90.0);
                    }
                    else
                    {
                        this.Skew = new Vector(0.0, num4 + 90.0);
                    }
                }
            }
        }
    }
    
    
    

    private double GetAngle(Vector v1, Vector v2)
    {
        return ((Math.Atan2((v1.X * v2.Y) - (v1.Y * v2.X), (v1.X * v2.X) + (v1.Y * v2.Y)) * 180.0) / 3.1415926535897931);
    }

    Wednesday, July 23, 2008 1:17 AM
  • This is excellent. I haven't tried to understand all of the assumptions in the code either, but this is exactly what I was hoping for. I will try to set up some unit tests with a variety of transforms to see if there are any limitations or issues. Thank you for your effort.

    Tuesday, July 29, 2008 10:42 AM